@webapper/cloudsee-drive-mcp 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +34 -0
- package/LICENSE +21 -0
- package/README.md +146 -0
- package/contract/registry.snapshot.json +542 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +861 -0
- package/dist/index.js.map +1 -0
- package/package.json +79 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,861 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5
|
+
|
|
6
|
+
// src/config.ts
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
|
|
9
|
+
// src/errors.ts
|
|
10
|
+
var CloudSeeError = class extends Error {
|
|
11
|
+
code;
|
|
12
|
+
status;
|
|
13
|
+
retryable;
|
|
14
|
+
constructor(message, options = {}) {
|
|
15
|
+
super(message);
|
|
16
|
+
this.name = "CloudSeeError";
|
|
17
|
+
this.code = options.code ?? "error";
|
|
18
|
+
this.status = options.status;
|
|
19
|
+
this.retryable = options.retryable ?? false;
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
var AuthError = class extends CloudSeeError {
|
|
23
|
+
constructor(message, options = {}) {
|
|
24
|
+
super(message, { ...options, code: options.code ?? "auth" });
|
|
25
|
+
this.name = "AuthError";
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// src/config.ts
|
|
30
|
+
var DEFAULT_BASE_URL = "https://drive-api.cloudsee.cloud";
|
|
31
|
+
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
32
|
+
var envSchema = z.object({
|
|
33
|
+
CLOUDSEE_API_KEY_ID: z.string().trim().min(1, "is required (your CloudSee Drive API key id)"),
|
|
34
|
+
CLOUDSEE_API_KEY_SECRET: z.string().trim().min(1, "is required (the matching API key secret)"),
|
|
35
|
+
CLOUDSEE_API_BASE_URL: z.string().url("must be a valid URL").optional(),
|
|
36
|
+
CLOUDSEE_TIMEOUT_MS: z.coerce.number().int().positive().optional(),
|
|
37
|
+
CLOUDSEE_LOG_LEVEL: z.enum(["error", "warn", "info", "debug"]).optional()
|
|
38
|
+
});
|
|
39
|
+
function loadConfig(env = process.env) {
|
|
40
|
+
const parsed = envSchema.safeParse(env);
|
|
41
|
+
if (!parsed.success) {
|
|
42
|
+
const issues = parsed.error.issues.map((i) => ` \u2022 ${i.path.join(".")} ${i.message}`).join("\n");
|
|
43
|
+
throw new CloudSeeError(
|
|
44
|
+
`CloudSee Drive MCP server is not configured:
|
|
45
|
+
${issues}
|
|
46
|
+
|
|
47
|
+
Set the required environment variables (see .env.example or the README).`,
|
|
48
|
+
{ code: "config" }
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
const e = parsed.data;
|
|
52
|
+
return {
|
|
53
|
+
apiKeyId: e.CLOUDSEE_API_KEY_ID,
|
|
54
|
+
apiKeySecret: e.CLOUDSEE_API_KEY_SECRET,
|
|
55
|
+
baseUrl: (e.CLOUDSEE_API_BASE_URL ?? DEFAULT_BASE_URL).replace(/\/+$/, ""),
|
|
56
|
+
timeoutMs: e.CLOUDSEE_TIMEOUT_MS ?? DEFAULT_TIMEOUT_MS,
|
|
57
|
+
logLevel: e.CLOUDSEE_LOG_LEVEL ?? "info"
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// src/logger.ts
|
|
62
|
+
var LEVEL_WEIGHT = { error: 0, warn: 1, info: 2, debug: 3 };
|
|
63
|
+
var currentLevel = "info";
|
|
64
|
+
var secrets = [];
|
|
65
|
+
function configureLogger(options) {
|
|
66
|
+
if (options.level) currentLevel = options.level;
|
|
67
|
+
if (options.redact) {
|
|
68
|
+
secrets = options.redact.filter((s) => typeof s === "string" && s.length > 0);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
function redactSecrets(text) {
|
|
72
|
+
let out = text;
|
|
73
|
+
for (const secret of secrets) {
|
|
74
|
+
if (out.includes(secret)) out = out.split(secret).join("***");
|
|
75
|
+
}
|
|
76
|
+
return out;
|
|
77
|
+
}
|
|
78
|
+
function stringify(meta) {
|
|
79
|
+
if (meta === void 0) return "";
|
|
80
|
+
if (typeof meta === "string") return meta;
|
|
81
|
+
try {
|
|
82
|
+
return JSON.stringify(meta);
|
|
83
|
+
} catch {
|
|
84
|
+
return String(meta);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
function emit(level, message, meta) {
|
|
88
|
+
if (LEVEL_WEIGHT[level] > LEVEL_WEIGHT[currentLevel]) return;
|
|
89
|
+
const suffix = meta === void 0 ? "" : ` ${stringify(meta)}`;
|
|
90
|
+
process.stderr.write(`[cloudsee-drive-mcp] ${level.toUpperCase()}: ${redactSecrets(message + suffix)}
|
|
91
|
+
`);
|
|
92
|
+
}
|
|
93
|
+
var logger = {
|
|
94
|
+
error: (message, meta) => emit("error", message, meta),
|
|
95
|
+
warn: (message, meta) => emit("warn", message, meta),
|
|
96
|
+
info: (message, meta) => emit("info", message, meta),
|
|
97
|
+
debug: (message, meta) => emit("debug", message, meta)
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
// src/server.ts
|
|
101
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
102
|
+
|
|
103
|
+
// src/version.ts
|
|
104
|
+
var VERSION = "0.1.0";
|
|
105
|
+
|
|
106
|
+
// src/client/pagination.ts
|
|
107
|
+
function encodeCursor(dialect, value) {
|
|
108
|
+
if (value === null || value === void 0 || value === "") return void 0;
|
|
109
|
+
const payload = { d: dialect, v: String(value) };
|
|
110
|
+
return Buffer.from(JSON.stringify(payload), "utf8").toString("base64url");
|
|
111
|
+
}
|
|
112
|
+
function decodeCursor(cursor, expected) {
|
|
113
|
+
let payload;
|
|
114
|
+
try {
|
|
115
|
+
payload = JSON.parse(Buffer.from(cursor, "base64url").toString("utf8"));
|
|
116
|
+
} catch {
|
|
117
|
+
throw new CloudSeeError("Invalid pagination cursor.", { code: "bad_cursor" });
|
|
118
|
+
}
|
|
119
|
+
if (!payload || typeof payload.v !== "string" || payload.d !== expected) {
|
|
120
|
+
throw new CloudSeeError("Pagination cursor is not valid for this operation.", { code: "bad_cursor" });
|
|
121
|
+
}
|
|
122
|
+
return payload.v;
|
|
123
|
+
}
|
|
124
|
+
var RESPONSE_FIELDS = ["nextPage", "nextPageToken", "marker", "lastEvaluatedKey", "pageToken", "nextToken"];
|
|
125
|
+
function extractNextToken(data, dialect) {
|
|
126
|
+
if (!data || typeof data !== "object") return void 0;
|
|
127
|
+
const record = data;
|
|
128
|
+
for (const field of [dialect, ...RESPONSE_FIELDS]) {
|
|
129
|
+
const value = record[field];
|
|
130
|
+
if (typeof value === "string" && value !== "") return value;
|
|
131
|
+
if (value && typeof value === "object") {
|
|
132
|
+
try {
|
|
133
|
+
return JSON.stringify(value);
|
|
134
|
+
} catch {
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return void 0;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// src/client/CloudSeeClient.ts
|
|
142
|
+
var DEFAULT_RETRIES = 3;
|
|
143
|
+
var BASE_BACKOFF_MS = 300;
|
|
144
|
+
var MAX_BACKOFF_MS = 8e3;
|
|
145
|
+
var CloudSeeClient = class {
|
|
146
|
+
baseUrl;
|
|
147
|
+
timeoutMs;
|
|
148
|
+
secret;
|
|
149
|
+
headers;
|
|
150
|
+
constructor(config) {
|
|
151
|
+
this.baseUrl = config.baseUrl;
|
|
152
|
+
this.timeoutMs = config.timeoutMs;
|
|
153
|
+
this.secret = config.apiKeySecret;
|
|
154
|
+
this.headers = {
|
|
155
|
+
"Content-Type": "application/json",
|
|
156
|
+
Accept: "application/json",
|
|
157
|
+
"X-Api-Key-Id": config.apiKeyId,
|
|
158
|
+
"X-Api-Key-Secret": config.apiKeySecret,
|
|
159
|
+
"User-Agent": `cloudsee-drive-mcp/${VERSION}`
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
/** POST a data-plane RPC endpoint (path WITHOUT `/v1`). Returns unwrapped `data`. */
|
|
163
|
+
async post(path, body = {}, options = {}) {
|
|
164
|
+
return (await this.request(path, body, options)).data;
|
|
165
|
+
}
|
|
166
|
+
/** POST a paginated endpoint. Translates the opaque `cursor` to/from the
|
|
167
|
+
* endpoint's pagination dialect and returns a normalized `nextCursor`. The
|
|
168
|
+
* continuation token can sit at the envelope level (e.g. `/storage/recent`
|
|
169
|
+
* returns `nextToken` as a sibling of `data`) or inside `data` — check the
|
|
170
|
+
* envelope first, then `data`, so a token is never silently dropped. */
|
|
171
|
+
async postPaged(path, body, dialect, cursor, options = {}) {
|
|
172
|
+
const requestBody = { ...body };
|
|
173
|
+
if (cursor) requestBody[dialect] = decodeCursor(cursor, dialect);
|
|
174
|
+
const { data, envelope } = await this.request(path, requestBody, options);
|
|
175
|
+
const token = extractNextToken(envelope, dialect) ?? extractNextToken(data, dialect);
|
|
176
|
+
return { data, nextCursor: encodeCursor(dialect, token) };
|
|
177
|
+
}
|
|
178
|
+
/** Run the request with retry/backoff, returning the unwrapped `data` together
|
|
179
|
+
* with the raw response envelope (needed for envelope-level pagination tokens). */
|
|
180
|
+
async request(path, body, options) {
|
|
181
|
+
const url = `${this.baseUrl}/v1${path}`;
|
|
182
|
+
const retries = options.retries ?? DEFAULT_RETRIES;
|
|
183
|
+
let attempt = 0;
|
|
184
|
+
for (; ; ) {
|
|
185
|
+
attempt += 1;
|
|
186
|
+
const response = await this.fetchOnce(url, path, body, options.signal);
|
|
187
|
+
if (response.status === 401 || response.status === 403) {
|
|
188
|
+
throw new AuthError(
|
|
189
|
+
"Authentication failed (HTTP " + response.status + "). Check CLOUDSEE_API_KEY_ID and CLOUDSEE_API_KEY_SECRET, and that the key is active and not expired.",
|
|
190
|
+
{ status: response.status }
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
if ((response.status === 429 || response.status >= 500) && attempt <= retries) {
|
|
194
|
+
const waitMs = retryAfterMs(response.headers.get("retry-after")) ?? backoffMs(attempt);
|
|
195
|
+
logger.warn(`HTTP ${response.status} from ${path}; retrying in ${waitMs}ms (attempt ${attempt}/${retries}).`);
|
|
196
|
+
await sleep(waitMs);
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
return await this.parse(response, path);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
async fetchOnce(url, path, body, external) {
|
|
203
|
+
const controller = new AbortController();
|
|
204
|
+
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
205
|
+
const onAbort = () => controller.abort();
|
|
206
|
+
if (external) external.addEventListener("abort", onAbort, { once: true });
|
|
207
|
+
try {
|
|
208
|
+
return await fetch(url, {
|
|
209
|
+
method: "POST",
|
|
210
|
+
headers: this.headers,
|
|
211
|
+
body: JSON.stringify(body),
|
|
212
|
+
signal: controller.signal
|
|
213
|
+
});
|
|
214
|
+
} catch (err) {
|
|
215
|
+
const reason = controller.signal.aborted ? `timed out after ${this.timeoutMs}ms` : this.redact(errMessage(err));
|
|
216
|
+
throw new CloudSeeError(`Network error calling ${path}: ${reason}`, { code: "network", retryable: true });
|
|
217
|
+
} finally {
|
|
218
|
+
clearTimeout(timer);
|
|
219
|
+
if (external) external.removeEventListener("abort", onAbort);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
async parse(response, path) {
|
|
223
|
+
const text = await response.text();
|
|
224
|
+
let json;
|
|
225
|
+
if (text) {
|
|
226
|
+
try {
|
|
227
|
+
json = JSON.parse(text);
|
|
228
|
+
} catch {
|
|
229
|
+
throw new CloudSeeError(
|
|
230
|
+
response.ok ? `CloudSee API returned a non-JSON response for ${path}.` : `CloudSee API returned HTTP ${response.status} for ${path}.`,
|
|
231
|
+
{ status: response.status, code: response.ok ? "bad_response" : "http_error" }
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
if (!response.ok) {
|
|
236
|
+
const message = json?.errorMessage ?? `HTTP ${response.status}`;
|
|
237
|
+
throw new CloudSeeError(`CloudSee API error for ${path}: ${this.redact(message)}`, {
|
|
238
|
+
status: response.status,
|
|
239
|
+
code: json?.code ?? "http_error"
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
if (json && typeof json === "object" && json.success === false) {
|
|
243
|
+
throw new CloudSeeError(this.redact(json.errorMessage || `Operation failed (${json.code ?? "unknown"}).`), {
|
|
244
|
+
code: json.code ?? "operation_failed",
|
|
245
|
+
status: response.status
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
const envelope = json && typeof json === "object" ? json : void 0;
|
|
249
|
+
if (json && typeof json === "object" && "data" in json) return { data: json.data, envelope };
|
|
250
|
+
return { data: json ?? {}, envelope };
|
|
251
|
+
}
|
|
252
|
+
redact(text) {
|
|
253
|
+
return this.secret && text.includes(this.secret) ? text.split(this.secret).join("***") : text;
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
function errMessage(err) {
|
|
257
|
+
return err instanceof Error ? err.message : String(err);
|
|
258
|
+
}
|
|
259
|
+
function backoffMs(attempt) {
|
|
260
|
+
const ceiling = Math.min(MAX_BACKOFF_MS, BASE_BACKOFF_MS * 2 ** (attempt - 1));
|
|
261
|
+
return Math.round(ceiling / 2 + Math.random() * (ceiling / 2));
|
|
262
|
+
}
|
|
263
|
+
function retryAfterMs(header) {
|
|
264
|
+
if (!header) return void 0;
|
|
265
|
+
const seconds = Number(header);
|
|
266
|
+
if (Number.isFinite(seconds)) return Math.max(0, seconds * 1e3);
|
|
267
|
+
const date = Date.parse(header);
|
|
268
|
+
if (Number.isFinite(date)) return Math.max(0, date - Date.now());
|
|
269
|
+
return void 0;
|
|
270
|
+
}
|
|
271
|
+
function sleep(ms) {
|
|
272
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// src/tools/read.ts
|
|
276
|
+
import { z as z2 } from "zod";
|
|
277
|
+
|
|
278
|
+
// src/tools/format.ts
|
|
279
|
+
var DEFAULT_MAX_ITEMS = 100;
|
|
280
|
+
var DEFAULT_MAX_CHARS = 8e3;
|
|
281
|
+
function summarize(data, opts = {}) {
|
|
282
|
+
const maxItems = opts.maxItems ?? DEFAULT_MAX_ITEMS;
|
|
283
|
+
const maxChars = opts.maxChars ?? DEFAULT_MAX_CHARS;
|
|
284
|
+
let json;
|
|
285
|
+
try {
|
|
286
|
+
json = JSON.stringify(boundArrays(data, maxItems), null, 2);
|
|
287
|
+
} catch {
|
|
288
|
+
json = String(data);
|
|
289
|
+
}
|
|
290
|
+
if (json.length > maxChars) {
|
|
291
|
+
json = `${json.slice(0, maxChars)}
|
|
292
|
+
\u2026 (output truncated at ${maxChars} characters \u2014 narrow your query or paginate).`;
|
|
293
|
+
}
|
|
294
|
+
return json;
|
|
295
|
+
}
|
|
296
|
+
function boundArrays(value, maxItems) {
|
|
297
|
+
if (Array.isArray(value)) {
|
|
298
|
+
const capped = value.slice(0, maxItems).map((v) => boundArrays(v, maxItems));
|
|
299
|
+
if (value.length > maxItems) {
|
|
300
|
+
capped.push(`\u2026 ${value.length - maxItems} more item(s) omitted \u2014 paginate for the rest.`);
|
|
301
|
+
}
|
|
302
|
+
return capped;
|
|
303
|
+
}
|
|
304
|
+
if (value && typeof value === "object") {
|
|
305
|
+
const out = {};
|
|
306
|
+
for (const [k, v] of Object.entries(value)) {
|
|
307
|
+
out[k] = boundArrays(v, maxItems);
|
|
308
|
+
}
|
|
309
|
+
return out;
|
|
310
|
+
}
|
|
311
|
+
return value;
|
|
312
|
+
}
|
|
313
|
+
function withCursor(body, nextCursor) {
|
|
314
|
+
if (!nextCursor) return body;
|
|
315
|
+
return `${body}
|
|
316
|
+
|
|
317
|
+
\u21AA More results available. Call this tool again with cursor="${nextCursor}" for the next page.`;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// src/tools/types.ts
|
|
321
|
+
function textResult(text) {
|
|
322
|
+
return { content: [{ type: "text", text }] };
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// src/tools/read.ts
|
|
326
|
+
var listBuckets = {
|
|
327
|
+
name: "list_buckets",
|
|
328
|
+
title: "List buckets",
|
|
329
|
+
description: "List the storage buckets (Amazon S3 buckets) the authenticated CloudSee Drive account can access. Use this first to discover available buckets before browsing or searching.",
|
|
330
|
+
endpoint: { method: "POST", path: "/storage/buckets", scopes: ["drive:read"] },
|
|
331
|
+
inputSchema: {},
|
|
332
|
+
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
333
|
+
handler: async (_args, { client }) => {
|
|
334
|
+
const data = await client.post("/storage/buckets", {});
|
|
335
|
+
return textResult(summarize(data));
|
|
336
|
+
}
|
|
337
|
+
};
|
|
338
|
+
var browseSchema = z2.object({
|
|
339
|
+
path: z2.string().optional().describe("Folder path / prefix to list. Empty or omitted = the root."),
|
|
340
|
+
sortOption: z2.string().optional().describe("Sort key, e.g. 'name_asc', 'name_desc', 'date_desc'."),
|
|
341
|
+
pageSize: z2.number().int().min(1).max(200).optional().describe("Max items per page (1-200, default 50)."),
|
|
342
|
+
cursor: z2.string().optional().describe("Opaque pagination cursor returned by a previous call.")
|
|
343
|
+
});
|
|
344
|
+
var browseFolder = {
|
|
345
|
+
name: "browse_folder",
|
|
346
|
+
title: "Browse folder",
|
|
347
|
+
description: "List the files and sub-folders directly inside a folder (one level, non-recursive), with sorting and pagination. To filter by keyword use 'search_files'.",
|
|
348
|
+
endpoint: { method: "POST", path: "/storage/list", scopes: ["drive:read"] },
|
|
349
|
+
inputSchema: browseSchema.shape,
|
|
350
|
+
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
351
|
+
handler: async (args, { client }) => {
|
|
352
|
+
const a = browseSchema.parse(args);
|
|
353
|
+
const { data, nextCursor } = await client.postPaged(
|
|
354
|
+
"/storage/list",
|
|
355
|
+
{ dirPath: a.path ?? "", sortOption: a.sortOption, pageSize: a.pageSize ?? 50 },
|
|
356
|
+
"nextPage",
|
|
357
|
+
a.cursor
|
|
358
|
+
);
|
|
359
|
+
return textResult(withCursor(summarize(data), nextCursor));
|
|
360
|
+
}
|
|
361
|
+
};
|
|
362
|
+
var searchSchema = z2.object({
|
|
363
|
+
query: z2.string().min(1).describe("Keyword to match against file and folder names."),
|
|
364
|
+
path: z2.string().optional().describe("Folder to search within (this folder level, non-recursive). Empty = root."),
|
|
365
|
+
pageSize: z2.number().int().min(1).max(200).optional().describe("Max items per page (1-200, default 50)."),
|
|
366
|
+
cursor: z2.string().optional().describe("Opaque pagination cursor returned by a previous call.")
|
|
367
|
+
});
|
|
368
|
+
var searchFiles = {
|
|
369
|
+
name: "search_files",
|
|
370
|
+
title: "Search files",
|
|
371
|
+
description: "Search for files and folders by name keyword within a folder. Note: this matches the given folder level and is NOT a whole-tree recursive search \u2014 browse into sub-folders to search deeper. Returns matches with pagination.",
|
|
372
|
+
endpoint: { method: "POST", path: "/storage/list", scopes: ["drive:read"] },
|
|
373
|
+
inputSchema: searchSchema.shape,
|
|
374
|
+
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
375
|
+
handler: async (args, { client }) => {
|
|
376
|
+
const a = searchSchema.parse(args);
|
|
377
|
+
const { data, nextCursor } = await client.postPaged(
|
|
378
|
+
"/storage/list",
|
|
379
|
+
{ dirPath: a.path ?? "", searchingKeyword: a.query, pageSize: a.pageSize ?? 50 },
|
|
380
|
+
"nextPage",
|
|
381
|
+
a.cursor
|
|
382
|
+
);
|
|
383
|
+
return textResult(withCursor(summarize(data), nextCursor));
|
|
384
|
+
}
|
|
385
|
+
};
|
|
386
|
+
var recentSchema = z2.object({
|
|
387
|
+
limit: z2.number().int().min(1).max(200).optional().describe("Max items (1-200, default 50)."),
|
|
388
|
+
cursor: z2.string().optional().describe("Opaque pagination cursor returned by a previous call.")
|
|
389
|
+
});
|
|
390
|
+
var recentFiles = {
|
|
391
|
+
name: "recent_files",
|
|
392
|
+
title: "Recent files",
|
|
393
|
+
description: "List the account's most recently accessed or modified files, newest first, with pagination.",
|
|
394
|
+
endpoint: { method: "POST", path: "/storage/recent", scopes: ["drive:read"] },
|
|
395
|
+
inputSchema: recentSchema.shape,
|
|
396
|
+
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
397
|
+
handler: async (args, { client }) => {
|
|
398
|
+
const a = recentSchema.parse(args);
|
|
399
|
+
const limit = a.limit ?? 50;
|
|
400
|
+
const { data, nextCursor } = await client.postPaged("/storage/recent", { limit }, "nextToken", a.cursor);
|
|
401
|
+
const hasMore = Array.isArray(data) && data.length >= limit;
|
|
402
|
+
return textResult(withCursor(summarize(data), hasMore ? nextCursor : void 0));
|
|
403
|
+
}
|
|
404
|
+
};
|
|
405
|
+
var metaSchema = z2.object({ objectKey: z2.string().min(1).describe("Full object key (path) of the file.") });
|
|
406
|
+
var getFileMetadata = {
|
|
407
|
+
name: "get_file_metadata",
|
|
408
|
+
title: "Get file metadata",
|
|
409
|
+
description: "Get detailed metadata for a single file or object (size, type, timestamps, storage class, and other attributes) by its object key.",
|
|
410
|
+
endpoint: { method: "POST", path: "/storage/object/detail", scopes: ["drive:read"] },
|
|
411
|
+
inputSchema: metaSchema.shape,
|
|
412
|
+
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
413
|
+
handler: async (args, { client }) => {
|
|
414
|
+
const a = metaSchema.parse(args);
|
|
415
|
+
const data = await client.post("/storage/object/detail", { objectKey: a.objectKey });
|
|
416
|
+
return textResult(summarize(data));
|
|
417
|
+
}
|
|
418
|
+
};
|
|
419
|
+
var tagsSchema = z2.object({
|
|
420
|
+
objectKey: z2.string().min(1).describe("Full object key (path) of the file."),
|
|
421
|
+
bucketName: z2.string().optional().describe("Bucket name, if not implied by the key.")
|
|
422
|
+
});
|
|
423
|
+
var getFileTags = {
|
|
424
|
+
name: "get_file_tags",
|
|
425
|
+
title: "Get file tags",
|
|
426
|
+
description: "Get the S3 object tags (key/value pairs) attached to a file, by object key.",
|
|
427
|
+
endpoint: { method: "POST", path: "/storage/object/tagging", scopes: ["drive:read"] },
|
|
428
|
+
inputSchema: tagsSchema.shape,
|
|
429
|
+
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
430
|
+
handler: async (args, { client }) => {
|
|
431
|
+
const a = tagsSchema.parse(args);
|
|
432
|
+
const data = await client.post("/storage/object/tagging", { objectKey: a.objectKey, bucketName: a.bucketName });
|
|
433
|
+
return textResult(summarize(data));
|
|
434
|
+
}
|
|
435
|
+
};
|
|
436
|
+
var readTools = [
|
|
437
|
+
listBuckets,
|
|
438
|
+
browseFolder,
|
|
439
|
+
searchFiles,
|
|
440
|
+
recentFiles,
|
|
441
|
+
getFileMetadata,
|
|
442
|
+
getFileTags
|
|
443
|
+
];
|
|
444
|
+
|
|
445
|
+
// src/tools/download.ts
|
|
446
|
+
import { z as z3 } from "zod";
|
|
447
|
+
var downloadSchema = z3.object({
|
|
448
|
+
filePath: z3.string().min(1).describe("Object key (path) of the file to download."),
|
|
449
|
+
forceDownload: z3.boolean().optional().describe("If true, the link forces an attachment download instead of inline view."),
|
|
450
|
+
storageId: z3.string().optional().describe("Optional storage/index id.")
|
|
451
|
+
});
|
|
452
|
+
var downloadFile = {
|
|
453
|
+
name: "download_file",
|
|
454
|
+
title: "Download file",
|
|
455
|
+
description: "Get a short-lived pre-signed download URL for a file. The URL is time-limited and grants read access to that one object \u2014 share it with care. Returns the URL; it does not load file bytes into the conversation.",
|
|
456
|
+
endpoint: { method: "POST", path: "/storage/object/download-url", scopes: ["drive:read", "drive:download"] },
|
|
457
|
+
inputSchema: downloadSchema.shape,
|
|
458
|
+
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
459
|
+
handler: async (args, { client }) => {
|
|
460
|
+
const a = downloadSchema.parse(args);
|
|
461
|
+
const data = await client.post("/storage/object/download-url", {
|
|
462
|
+
filePath: a.filePath,
|
|
463
|
+
download: a.forceDownload ?? false,
|
|
464
|
+
storageId: a.storageId
|
|
465
|
+
});
|
|
466
|
+
return textResult(summarize(data));
|
|
467
|
+
}
|
|
468
|
+
};
|
|
469
|
+
var shareSchema = z3.object({
|
|
470
|
+
filePath: z3.string().min(1).describe("Object key (path) of the file to share."),
|
|
471
|
+
storageId: z3.string().optional().describe("Optional storage/index id.")
|
|
472
|
+
});
|
|
473
|
+
var shareLink = {
|
|
474
|
+
name: "share_link",
|
|
475
|
+
title: "Create share link",
|
|
476
|
+
description: "Create a shareable, time-limited link to a file (a pre-signed URL suitable for sharing). For richer share management (revocation, folder/prefix shares, expiry control) use the CloudSee dashboard.",
|
|
477
|
+
endpoint: { method: "POST", path: "/storage/object/download-url", scopes: ["drive:read", "drive:download"] },
|
|
478
|
+
inputSchema: shareSchema.shape,
|
|
479
|
+
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
480
|
+
handler: async (args, { client }) => {
|
|
481
|
+
const a = shareSchema.parse(args);
|
|
482
|
+
const data = await client.post("/storage/object/download-url", {
|
|
483
|
+
filePath: a.filePath,
|
|
484
|
+
shareableLink: true,
|
|
485
|
+
storageId: a.storageId
|
|
486
|
+
});
|
|
487
|
+
return textResult(summarize(data));
|
|
488
|
+
}
|
|
489
|
+
};
|
|
490
|
+
var downloadTools = [downloadFile, shareLink];
|
|
491
|
+
|
|
492
|
+
// src/tools/write.ts
|
|
493
|
+
import { readFile, stat } from "fs/promises";
|
|
494
|
+
import { basename } from "path";
|
|
495
|
+
import { z as z5 } from "zod";
|
|
496
|
+
|
|
497
|
+
// src/confirm.ts
|
|
498
|
+
import { z as z4 } from "zod";
|
|
499
|
+
var confirmShape = {
|
|
500
|
+
confirm: z4.boolean().optional().describe(
|
|
501
|
+
"Must be true to actually perform this mutating/irreversible action. If omitted or false, the tool returns a preview and makes no changes."
|
|
502
|
+
)
|
|
503
|
+
};
|
|
504
|
+
var SECURITY_NOTE = "Note: this confirmation is a client-side safety prompt, not the security boundary \u2014 the CloudSee API authorizes every operation server-side.";
|
|
505
|
+
function confirmationPreview(action, details) {
|
|
506
|
+
return textResult(
|
|
507
|
+
`\u26A0\uFE0F Confirmation required \u2014 no changes have been made.
|
|
508
|
+
|
|
509
|
+
About to: ${action}
|
|
510
|
+
${details}
|
|
511
|
+
|
|
512
|
+
Re-run this tool with confirm=true to proceed.
|
|
513
|
+
${SECURITY_NOTE}`
|
|
514
|
+
);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// src/tools/write.ts
|
|
518
|
+
var GATED = " (Write access is authorized server-side; until CloudSee's public-API RBAC wiring (CSD-572) is live, the gateway may deny this \u2014 that is expected, not a tool failure.)";
|
|
519
|
+
var MULTIPART_THRESHOLD = 8 * 1024 * 1024;
|
|
520
|
+
function pickUrl(data) {
|
|
521
|
+
if (typeof data === "string") return data;
|
|
522
|
+
if (data && typeof data === "object") {
|
|
523
|
+
const record = data;
|
|
524
|
+
for (const key of ["url", "uploadUrl", "signedUrl", "preSignedUrl", "presignedUrl", "putUrl"]) {
|
|
525
|
+
const value = record[key];
|
|
526
|
+
if (typeof value === "string" && value) return value;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
return void 0;
|
|
530
|
+
}
|
|
531
|
+
function pickKey(data) {
|
|
532
|
+
if (data && typeof data === "object") {
|
|
533
|
+
const record = data;
|
|
534
|
+
for (const key of ["key", "objectKey", "Key", "objectPath", "filePath"]) {
|
|
535
|
+
const value = record[key];
|
|
536
|
+
if (typeof value === "string" && value) return value;
|
|
537
|
+
}
|
|
538
|
+
const fields = record["fields"];
|
|
539
|
+
if (fields && typeof fields === "object") {
|
|
540
|
+
const nested = fields["key"];
|
|
541
|
+
if (typeof nested === "string" && nested) return nested;
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
return void 0;
|
|
545
|
+
}
|
|
546
|
+
var uploadSchema = z5.object({
|
|
547
|
+
localPath: z5.string().min(1).describe("Path to the local file to upload (absolute, or relative to the server's working directory)."),
|
|
548
|
+
destinationFolder: z5.string().describe("Destination folder/prefix in CloudSee Drive. Empty = root."),
|
|
549
|
+
fileName: z5.string().optional().describe("Name to store the file as. Defaults to the local file's name."),
|
|
550
|
+
contentType: z5.string().optional().describe("MIME type. Defaults to application/octet-stream."),
|
|
551
|
+
storageClass: z5.string().optional().describe("Optional S3 storage class (e.g. STANDARD, INTELLIGENT_TIERING).")
|
|
552
|
+
});
|
|
553
|
+
var uploadFile = {
|
|
554
|
+
name: "upload_file",
|
|
555
|
+
title: "Upload file",
|
|
556
|
+
description: "Upload a local file to a CloudSee Drive folder. Reads the file from the local machine, requests a pre-signed upload URL, uploads the bytes to storage, then finalizes the object." + GATED,
|
|
557
|
+
endpoint: { method: "POST", path: "/storage/upload/url", scopes: ["drive:write"] },
|
|
558
|
+
inputSchema: uploadSchema.shape,
|
|
559
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
560
|
+
handler: async (args, { client }) => {
|
|
561
|
+
const a = uploadSchema.parse(args);
|
|
562
|
+
const fileName = a.fileName ?? basename(a.localPath);
|
|
563
|
+
const contentType = a.contentType ?? "application/octet-stream";
|
|
564
|
+
let info;
|
|
565
|
+
try {
|
|
566
|
+
info = await stat(a.localPath);
|
|
567
|
+
} catch {
|
|
568
|
+
throw new CloudSeeError(`Local file not found: ${a.localPath}`, { code: "file_not_found" });
|
|
569
|
+
}
|
|
570
|
+
if (!info.isFile()) throw new CloudSeeError(`Not a regular file: ${a.localPath}`, { code: "not_a_file" });
|
|
571
|
+
if (info.size > MULTIPART_THRESHOLD) {
|
|
572
|
+
throw new CloudSeeError(
|
|
573
|
+
`File is ${(info.size / 1048576).toFixed(1)} MiB. Multipart upload (>8 MiB) is not enabled in this release \u2014 upload a smaller file or use the CloudSee web app for large files.`,
|
|
574
|
+
{ code: "too_large" }
|
|
575
|
+
);
|
|
576
|
+
}
|
|
577
|
+
const bytes = await readFile(a.localPath);
|
|
578
|
+
const presign = await client.post("/storage/upload/url", {
|
|
579
|
+
dirPath: a.destinationFolder,
|
|
580
|
+
fileName,
|
|
581
|
+
contentType,
|
|
582
|
+
storageClass: a.storageClass
|
|
583
|
+
});
|
|
584
|
+
const uploadUrl = pickUrl(presign);
|
|
585
|
+
if (!uploadUrl) throw new CloudSeeError("The API did not return a usable upload URL.", { code: "no_upload_url" });
|
|
586
|
+
const key = pickKey(presign);
|
|
587
|
+
if (!key) {
|
|
588
|
+
throw new CloudSeeError(
|
|
589
|
+
"The upload-URL response did not include the object key, so the upload cannot be finalized safely. (No bytes were uploaded.)",
|
|
590
|
+
{ code: "no_object_key" }
|
|
591
|
+
);
|
|
592
|
+
}
|
|
593
|
+
const put = await fetch(uploadUrl, { method: "PUT", headers: { "Content-Type": contentType }, body: new Uint8Array(bytes) });
|
|
594
|
+
if (!put.ok) throw new CloudSeeError(`Upload to storage failed (HTTP ${put.status}).`, { status: put.status, code: "upload_failed" });
|
|
595
|
+
let complete;
|
|
596
|
+
try {
|
|
597
|
+
complete = await client.post("/storage/upload/complete", {
|
|
598
|
+
dirPath: a.destinationFolder,
|
|
599
|
+
objects: [{ fileName, key, contentType, size: info.size }]
|
|
600
|
+
});
|
|
601
|
+
} catch (err) {
|
|
602
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
603
|
+
throw new CloudSeeError(
|
|
604
|
+
`Bytes were uploaded to storage but finalization failed: ${reason}. The object may exist without an index entry \u2014 retry the upload, or remove it via the CloudSee app.`,
|
|
605
|
+
{ code: "finalize_failed" }
|
|
606
|
+
);
|
|
607
|
+
}
|
|
608
|
+
return textResult(`Uploaded "${fileName}" (${info.size} bytes) to "${a.destinationFolder || "/"}".
|
|
609
|
+
|
|
610
|
+
${summarize(complete)}`);
|
|
611
|
+
}
|
|
612
|
+
};
|
|
613
|
+
var createFolderSchema = z5.object({
|
|
614
|
+
parentPath: z5.string().describe("Parent folder/prefix. Empty = root."),
|
|
615
|
+
name: z5.string().min(1).describe("New folder name.")
|
|
616
|
+
});
|
|
617
|
+
var createFolder = {
|
|
618
|
+
name: "create_folder",
|
|
619
|
+
title: "Create folder",
|
|
620
|
+
description: "Create a new folder at the given path." + GATED,
|
|
621
|
+
endpoint: { method: "POST", path: "/storage/folder/create", scopes: ["drive:write"] },
|
|
622
|
+
inputSchema: createFolderSchema.shape,
|
|
623
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
624
|
+
handler: async (args, { client }) => {
|
|
625
|
+
const a = createFolderSchema.parse(args);
|
|
626
|
+
const data = await client.post("/storage/folder/create", { dirPath: a.parentPath, object: a.name });
|
|
627
|
+
return textResult(`Created folder "${a.name}" in "${a.parentPath || "/"}".
|
|
628
|
+
|
|
629
|
+
${summarize(data)}`);
|
|
630
|
+
}
|
|
631
|
+
};
|
|
632
|
+
var renameSchema = z5.object({
|
|
633
|
+
oldObjectId: z5.string().min(1).describe("Current object key/id of the file or folder."),
|
|
634
|
+
newName: z5.string().min(1).describe("New name."),
|
|
635
|
+
isFolder: z5.boolean().optional().describe("Set true when renaming a folder."),
|
|
636
|
+
storageId: z5.string().optional().describe("Optional storage/index id."),
|
|
637
|
+
...confirmShape
|
|
638
|
+
});
|
|
639
|
+
var renameFile = {
|
|
640
|
+
name: "rename_file",
|
|
641
|
+
title: "Rename file or folder",
|
|
642
|
+
description: "Rename a file or folder. Destructive (changes the object's key). Requires confirm=true." + GATED,
|
|
643
|
+
endpoint: { method: "POST", path: "/storage/object/rename", scopes: ["drive:write"] },
|
|
644
|
+
inputSchema: renameSchema.shape,
|
|
645
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true },
|
|
646
|
+
handler: async (args, { client }) => {
|
|
647
|
+
const a = renameSchema.parse(args);
|
|
648
|
+
if (!a.confirm) {
|
|
649
|
+
return confirmationPreview(`rename ${a.isFolder ? "folder" : "file"} to "${a.newName}"`, `Target: ${a.oldObjectId}`);
|
|
650
|
+
}
|
|
651
|
+
const data = await client.post("/storage/object/rename", {
|
|
652
|
+
oldObjectId: a.oldObjectId,
|
|
653
|
+
newObjectName: a.newName,
|
|
654
|
+
isFolder: a.isFolder ?? false,
|
|
655
|
+
storageId: a.storageId
|
|
656
|
+
});
|
|
657
|
+
return textResult(`Renamed to "${a.newName}".
|
|
658
|
+
|
|
659
|
+
${summarize(data)}`);
|
|
660
|
+
}
|
|
661
|
+
};
|
|
662
|
+
var moveSchema = z5.object({
|
|
663
|
+
sourcePath: z5.string().min(1).describe("Source object key/prefix."),
|
|
664
|
+
destinationPath: z5.string().min(1).describe("Destination prefix."),
|
|
665
|
+
asCopy: z5.boolean().optional().describe("Copy instead of move (copy is non-destructive; the source is kept)."),
|
|
666
|
+
isFolder: z5.boolean().optional().describe("Set true for a folder."),
|
|
667
|
+
destinationBucket: z5.string().optional().describe("Target bucket, if different."),
|
|
668
|
+
storageId: z5.string().optional().describe("Optional storage/index id."),
|
|
669
|
+
...confirmShape
|
|
670
|
+
});
|
|
671
|
+
var moveFile = {
|
|
672
|
+
name: "move_file",
|
|
673
|
+
title: "Move or copy file",
|
|
674
|
+
description: "Move (or copy, with asCopy=true) a file or folder to a new location. A move removes the source and is destructive, so it requires confirm=true; a copy does not." + GATED,
|
|
675
|
+
endpoint: { method: "POST", path: "/storage/object/move", scopes: ["drive:write"] },
|
|
676
|
+
inputSchema: moveSchema.shape,
|
|
677
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true },
|
|
678
|
+
handler: async (args, { client }) => {
|
|
679
|
+
const a = moveSchema.parse(args);
|
|
680
|
+
const isMove = !a.asCopy;
|
|
681
|
+
if (isMove && !a.confirm) {
|
|
682
|
+
return confirmationPreview(`move "${a.sourcePath}" \u2192 "${a.destinationPath}"`, "This removes the source. Pass asCopy=true to copy instead.");
|
|
683
|
+
}
|
|
684
|
+
const data = await client.post("/storage/object/move", {
|
|
685
|
+
asCopy: a.asCopy ?? false,
|
|
686
|
+
isFolder: a.isFolder ?? false,
|
|
687
|
+
sourcePath: a.sourcePath,
|
|
688
|
+
destinationPath: a.destinationPath,
|
|
689
|
+
destinationBucket: a.destinationBucket,
|
|
690
|
+
storageId: a.storageId
|
|
691
|
+
});
|
|
692
|
+
return textResult(`${isMove ? "Moved" : "Copied"} "${a.sourcePath}" \u2192 "${a.destinationPath}".
|
|
693
|
+
|
|
694
|
+
${summarize(data)}`);
|
|
695
|
+
}
|
|
696
|
+
};
|
|
697
|
+
var duplicateSchema = z5.object({
|
|
698
|
+
objectKey: z5.string().min(1).describe("Source object key to duplicate."),
|
|
699
|
+
storageId: z5.string().optional().describe("Optional storage/index id.")
|
|
700
|
+
});
|
|
701
|
+
var duplicateFile = {
|
|
702
|
+
name: "duplicate_file",
|
|
703
|
+
title: "Duplicate file",
|
|
704
|
+
description: "Create a copy of a file in the same location." + GATED,
|
|
705
|
+
endpoint: { method: "POST", path: "/storage/object/duplicate", scopes: ["drive:write"] },
|
|
706
|
+
inputSchema: duplicateSchema.shape,
|
|
707
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
708
|
+
handler: async (args, { client }) => {
|
|
709
|
+
const a = duplicateSchema.parse(args);
|
|
710
|
+
const data = await client.post("/storage/object/duplicate", { objectKey: a.objectKey, storageId: a.storageId });
|
|
711
|
+
return textResult(`Duplicated "${a.objectKey}".
|
|
712
|
+
|
|
713
|
+
${summarize(data)}`);
|
|
714
|
+
}
|
|
715
|
+
};
|
|
716
|
+
var deleteSchema = z5.object({
|
|
717
|
+
objectKeys: z5.array(z5.string().min(1)).min(1).describe("Object keys (paths) to permanently delete."),
|
|
718
|
+
...confirmShape
|
|
719
|
+
});
|
|
720
|
+
var deleteFiles = {
|
|
721
|
+
name: "delete_files",
|
|
722
|
+
title: "Delete files",
|
|
723
|
+
description: "Permanently delete one or more files/objects by key. Destructive and irreversible. Requires confirm=true." + GATED,
|
|
724
|
+
endpoint: { method: "POST", path: "/storage/objects/delete", scopes: ["drive:delete"] },
|
|
725
|
+
inputSchema: deleteSchema.shape,
|
|
726
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true },
|
|
727
|
+
handler: async (args, { client }) => {
|
|
728
|
+
const a = deleteSchema.parse(args);
|
|
729
|
+
if (!a.confirm) {
|
|
730
|
+
return confirmationPreview(`permanently delete ${a.objectKeys.length} object(s)`, a.objectKeys.map((k) => ` - ${k}`).join("\n"));
|
|
731
|
+
}
|
|
732
|
+
const data = await client.post("/storage/objects/delete", { deletedObjects: a.objectKeys });
|
|
733
|
+
return textResult(`Deleted ${a.objectKeys.length} object(s).
|
|
734
|
+
|
|
735
|
+
${summarize(data)}`);
|
|
736
|
+
}
|
|
737
|
+
};
|
|
738
|
+
var updateMetaSchema = z5.object({
|
|
739
|
+
objectKey: z5.string().min(1).describe("Object key whose metadata to update."),
|
|
740
|
+
metadata: z5.record(z5.string(), z5.unknown()).describe("Metadata fields to set (key/value map). Overwrites the listed fields."),
|
|
741
|
+
...confirmShape
|
|
742
|
+
});
|
|
743
|
+
var updateMetadata = {
|
|
744
|
+
name: "update_metadata",
|
|
745
|
+
title: "Update file metadata",
|
|
746
|
+
description: "Update a file's metadata. Destructive (overwrites the listed metadata fields). Requires confirm=true." + GATED,
|
|
747
|
+
endpoint: { method: "POST", path: "/storage/object/metadata", scopes: ["drive:write"] },
|
|
748
|
+
inputSchema: updateMetaSchema.shape,
|
|
749
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true },
|
|
750
|
+
handler: async (args, { client }) => {
|
|
751
|
+
const a = updateMetaSchema.parse(args);
|
|
752
|
+
if (!a.confirm) {
|
|
753
|
+
return confirmationPreview(`update metadata on "${a.objectKey}"`, `Fields: ${Object.keys(a.metadata).join(", ") || "(none)"}`);
|
|
754
|
+
}
|
|
755
|
+
const data = await client.post("/storage/object/metadata", { objectKey: a.objectKey, metadata: a.metadata });
|
|
756
|
+
return textResult(`Updated metadata on "${a.objectKey}".
|
|
757
|
+
|
|
758
|
+
${summarize(data)}`);
|
|
759
|
+
}
|
|
760
|
+
};
|
|
761
|
+
var restoreSchema = z5.object({
|
|
762
|
+
objectKey: z5.string().min(1).describe("Object key of the archived (Glacier) object to restore."),
|
|
763
|
+
days: z5.number().int().min(1).optional().describe("How many days to keep the restored copy available."),
|
|
764
|
+
retrievalTier: z5.enum(["Expedited", "Standard", "Bulk"]).optional().describe("Glacier retrieval tier (default Standard)."),
|
|
765
|
+
storageId: z5.string().optional().describe("Optional storage/index id."),
|
|
766
|
+
...confirmShape
|
|
767
|
+
});
|
|
768
|
+
var restoreArchivedFile = {
|
|
769
|
+
name: "restore_archived_file",
|
|
770
|
+
title: "Restore archived file",
|
|
771
|
+
description: "Begin restoring an archived (S3 Glacier) object so it can be downloaded. This is Glacier un-archiving \u2014 NOT recovery of a deleted file \u2014 and may incur retrieval cost and take minutes to hours. Requires confirm=true." + GATED,
|
|
772
|
+
endpoint: { method: "POST", path: "/storage/object/restore", scopes: ["drive:write"] },
|
|
773
|
+
inputSchema: restoreSchema.shape,
|
|
774
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true },
|
|
775
|
+
handler: async (args, { client }) => {
|
|
776
|
+
const a = restoreSchema.parse(args);
|
|
777
|
+
if (!a.confirm) {
|
|
778
|
+
return confirmationPreview(
|
|
779
|
+
`restore archived object "${a.objectKey}"`,
|
|
780
|
+
`Tier: ${a.retrievalTier ?? "Standard"}${a.days ? `, ${a.days} day(s)` : ""}. May incur retrieval cost.`
|
|
781
|
+
);
|
|
782
|
+
}
|
|
783
|
+
const data = await client.post("/storage/object/restore", {
|
|
784
|
+
objectKey: a.objectKey,
|
|
785
|
+
days: a.days,
|
|
786
|
+
retrievalTier: a.retrievalTier,
|
|
787
|
+
storageId: a.storageId
|
|
788
|
+
});
|
|
789
|
+
return textResult(`Restore initiated for "${a.objectKey}".
|
|
790
|
+
|
|
791
|
+
${summarize(data)}`);
|
|
792
|
+
}
|
|
793
|
+
};
|
|
794
|
+
var writeTools = [
|
|
795
|
+
uploadFile,
|
|
796
|
+
createFolder,
|
|
797
|
+
renameFile,
|
|
798
|
+
moveFile,
|
|
799
|
+
duplicateFile,
|
|
800
|
+
deleteFiles,
|
|
801
|
+
updateMetadata,
|
|
802
|
+
restoreArchivedFile
|
|
803
|
+
];
|
|
804
|
+
|
|
805
|
+
// src/tools/index.ts
|
|
806
|
+
var allTools = [...readTools, ...downloadTools, ...writeTools];
|
|
807
|
+
|
|
808
|
+
// src/server.ts
|
|
809
|
+
function createServer(config) {
|
|
810
|
+
const client = new CloudSeeClient(config);
|
|
811
|
+
const server = new McpServer({ name: "cloudsee-drive-mcp", version: VERSION });
|
|
812
|
+
for (const tool of allTools) {
|
|
813
|
+
server.registerTool(
|
|
814
|
+
tool.name,
|
|
815
|
+
{
|
|
816
|
+
title: tool.title,
|
|
817
|
+
description: tool.description,
|
|
818
|
+
inputSchema: tool.inputSchema,
|
|
819
|
+
annotations: { title: tool.title, ...tool.annotations }
|
|
820
|
+
},
|
|
821
|
+
async (args) => {
|
|
822
|
+
try {
|
|
823
|
+
return await tool.handler(args ?? {}, { client });
|
|
824
|
+
} catch (err) {
|
|
825
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
826
|
+
logger.error(`tool "${tool.name}" failed`, message);
|
|
827
|
+
return { content: [{ type: "text", text: `Error: ${message}` }], isError: true };
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
);
|
|
831
|
+
}
|
|
832
|
+
logger.info(`registered ${allTools.length} tools`);
|
|
833
|
+
return server;
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
// src/index.ts
|
|
837
|
+
function loadConfigOrExit() {
|
|
838
|
+
try {
|
|
839
|
+
return loadConfig();
|
|
840
|
+
} catch (err) {
|
|
841
|
+
process.stderr.write(`
|
|
842
|
+
${err instanceof Error ? err.message : String(err)}
|
|
843
|
+
|
|
844
|
+
`);
|
|
845
|
+
process.exit(1);
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
async function main() {
|
|
849
|
+
const config = loadConfigOrExit();
|
|
850
|
+
configureLogger({ level: config.logLevel, redact: [config.apiKeySecret] });
|
|
851
|
+
const server = createServer(config);
|
|
852
|
+
const transport = new StdioServerTransport();
|
|
853
|
+
await server.connect(transport);
|
|
854
|
+
logger.info(`cloudsee-drive-mcp v${VERSION} ready on stdio (base: ${config.baseUrl})`);
|
|
855
|
+
}
|
|
856
|
+
main().catch((err) => {
|
|
857
|
+
process.stderr.write(`Fatal: ${err instanceof Error ? err.message : String(err)}
|
|
858
|
+
`);
|
|
859
|
+
process.exit(1);
|
|
860
|
+
});
|
|
861
|
+
//# sourceMappingURL=index.js.map
|