mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-11-07 06:39:10 +08:00
[feat] add MCP file browsing and cancellable transfers
This commit is contained in:
@@ -174,7 +174,8 @@ export class NativeClient {
|
||||
return Promise.reject(new Failure("BUSY", false, true));
|
||||
const id = ++this.sequence;
|
||||
const sideEffect = method === "action" || method === "unlock" ||
|
||||
(method === "clipboard" && args.operation === "set");
|
||||
(method === "clipboard" && args.operation === "set") ||
|
||||
(method === "files" && ["upload", "download", "cancel"].includes(args.operation));
|
||||
const line = JSON.stringify({ ...args, id, method }) + "\n";
|
||||
if (Buffer.byteLength(line) > 65536)
|
||||
return Promise.reject(new Failure("MESSAGE_TOO_LARGE"));
|
||||
|
||||
+26
-2
@@ -124,7 +124,8 @@ export function createServer(
|
||||
inputSchema: schema,
|
||||
annotations: {
|
||||
readOnlyHint: !!d.readOnly,
|
||||
destructiveHint: ["crossdesk_action", "crossdesk_unlock"].includes(d.name),
|
||||
destructiveHint: ["crossdesk_action", "crossdesk_unlock", "crossdesk_clipboard_set",
|
||||
"crossdesk_files_transfer", "crossdesk_files_cancel"].includes(d.name),
|
||||
openWorldHint: true,
|
||||
},
|
||||
...(d.ui || d.app
|
||||
@@ -316,6 +317,28 @@ export function createServer(
|
||||
"Read the remote text clipboard within the authorized control session.",
|
||||
async (p, ctx) => output(await sessions.clipboard({ ...p, operation: "get" }, ctx.mcpReq.signal)),
|
||||
{ readOnly: true });
|
||||
const filePath = z.string().max(4096).refine((path) => !path.includes("\0"));
|
||||
add("crossdesk_files_list", z.strictObject({ session_id: id, path: filePath.default(""),
|
||||
request_id: z.int().min(1).max(4294967295).optional(), offset: z.int().min(0).default(0) }),
|
||||
"List a remote directory. Empty path selects the remote home and @roots lists filesystem roots. Results contain up to 256 entries. Continue with request_id and next_offset while present. A timed-out listing remains queryable with its request_id.",
|
||||
async (p, ctx) => output(await sessions.files({ ...p, operation: "list" }, ctx.mcpReq.signal)),
|
||||
{ readOnly: true });
|
||||
add("crossdesk_files_transfer", z.strictObject({ session_id: id, operation_id: id,
|
||||
direction: z.enum(["upload", "download"]), paths: z.array(filePath.min(1)).min(1).max(256),
|
||||
destination: filePath.min(1) }),
|
||||
"Upload local files/folders or download remote files/folders into an existing absolute destination directory. paths are absolute source paths. Returns a transfer operation to monitor with crossdesk_files_status. Reuse the same operation_id and arguments to query the original transfer. Existing destination files receive a distinct name on conflict.",
|
||||
async (p, ctx) => {
|
||||
const { direction, ...args } = p;
|
||||
return output(await sessions.files({ ...args, operation: direction }, ctx.mcpReq.signal));
|
||||
});
|
||||
add("crossdesk_files_status", z.strictObject({ session_id: id, operation_id: id,
|
||||
offset: z.int().min(0).default(0) }),
|
||||
"Get transfer progress, per-file completion and errors. Continue file entries with next_offset. Wait for completed, failed or cancelled before reporting a finished transfer.",
|
||||
async (p, ctx) => output(await sessions.files({ ...p, operation: "status" }, ctx.mcpReq.signal)),
|
||||
{ readOnly: true });
|
||||
add("crossdesk_files_cancel", z.strictObject({ session_id: id, operation_id: id }),
|
||||
"Cancel a transfer and remove its unfinished staging files. Completed files remain available.",
|
||||
async (p, ctx) => output(await sessions.files({ ...p, operation: "cancel" }, ctx.mcpReq.signal)));
|
||||
add("crossdesk_clipboard_set", z.strictObject({ session_id: id, operation_id: id,
|
||||
text: z.string().max(16384).refine((text) => Buffer.byteLength(text) <= 16384 && !text.includes("\0")) }),
|
||||
"Set or clear the remote text clipboard. Reuse the same operation_id to query the original result. Returns applied after the remote clipboard update.",
|
||||
@@ -414,7 +437,8 @@ export function createServer(
|
||||
inputSchema,
|
||||
annotations: {
|
||||
readOnlyHint: !!d.readOnly,
|
||||
destructiveHint: ["crossdesk_action", "crossdesk_unlock"].includes(d.name),
|
||||
destructiveHint: ["crossdesk_action", "crossdesk_unlock", "crossdesk_clipboard_set",
|
||||
"crossdesk_files_transfer", "crossdesk_files_cancel"].includes(d.name),
|
||||
openWorldHint: true,
|
||||
},
|
||||
...((d.ui || d.app) && apps(ctx)
|
||||
|
||||
@@ -473,6 +473,34 @@ export class Sessions {
|
||||
if (s.unlock && this.unlockPending(s.unlock)) throw new Failure("UNLOCK_PENDING");
|
||||
return s.native.request("clipboard", args, 7000, signal);
|
||||
}
|
||||
async files(p: Data, signal?: AbortSignal): Promise<Data> {
|
||||
const { session_id, ...args } = p;
|
||||
const s = this.session(session_id);
|
||||
this.checkUnlock(s);
|
||||
if (s.unlock && this.unlockPending(s.unlock) && !["status", "cancel"].includes(args.operation))
|
||||
throw new Failure("UNLOCK_PENDING");
|
||||
if (args.operation !== "list") return s.native.request("files", args, 7000, signal);
|
||||
const requestId = args.request_id ?? (await s.native.request("files",
|
||||
{ operation: "list", path: args.path ?? "" }, 7000, signal)).request_id;
|
||||
try {
|
||||
const deadline = Date.now() + 25000;
|
||||
while (true) {
|
||||
const result = await s.native.request("files", {
|
||||
operation: "list_result", request_id: requestId, offset: args.offset ?? 0,
|
||||
}, 7000, signal);
|
||||
if (result.state !== "pending") {
|
||||
if (result.next_offset == null)
|
||||
await s.native.request("files", { operation: "list_cancel", request_id: requestId });
|
||||
return result;
|
||||
}
|
||||
if (Date.now() >= deadline) return { request_id: requestId, state: "pending", wait_timed_out: true };
|
||||
await delay(100, undefined, { signal });
|
||||
}
|
||||
} catch (error) {
|
||||
await s.native.request("files", { operation: "list_cancel", request_id: requestId }).catch(() => {});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
async control(id: string, enabled: boolean) {
|
||||
if (!enabled) this.cancelUnlock(this.session(id));
|
||||
return this.session(id).native.request("control", { enabled });
|
||||
|
||||
Reference in New Issue
Block a user