yungle-mcp 0.3.0 → 0.3.2
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 +3 -3
- package/dist/index.mjs +55 -53
- package/dist/server.mjs +82 -68
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -61,7 +61,7 @@ Or by hand:
|
|
|
61
61
|
| `get_account`, `list_transfers`, `get_transfer`, `get_transfer_downloads`, `list_collections`, `get_collection`, `list_collection_files`, `list_folders`, `list_guests`, `list_contacts`, `list_upload_requests`, `get_upload_request` | read | Answer questions about your transfers, collections, contacts and upload requests |
|
|
62
62
|
| `get_download_links` | read | Signed, resumable download URLs for a link someone shared (`/t/…`, `/c/…`) or your own transfer or collection |
|
|
63
63
|
| `get_import_status` | write | How far the files Yungle is fetching from URLs have got |
|
|
64
|
-
| `create_transfer` |
|
|
64
|
+
| `create_transfer` | write | Prepare a draft and get one keyless `npx yungle-cli put …` command per file, for an assistant with a shell |
|
|
65
65
|
| `finalize_transfer` | write | Make a prepared draft a link. Emails nobody |
|
|
66
66
|
| `share_from_urls` | write | Share files that are already online (a presigned S3 link, a CDN URL) — Yungle fetches them, so any size works, hosted too. Emails nobody |
|
|
67
67
|
| `create_share_link` | write | Turn content the assistant has (up to 25 MB) into a link. Emails nobody |
|
|
@@ -91,7 +91,7 @@ can put that in the message of a transfer they send you. So:
|
|
|
91
91
|
## Not reachable
|
|
92
92
|
|
|
93
93
|
Your vault and end-to-end encrypted transfers: their keys are derived in the browser and never
|
|
94
|
-
sent to Yungle. See <https://yungle.co/
|
|
94
|
+
sent to Yungle. See <https://docs.yungle.co/unsupported>.
|
|
95
95
|
|
|
96
|
-
Docs: <https://yungle.co/
|
|
96
|
+
Docs: <https://docs.yungle.co/mcp-server?ref=npm> · Source:
|
|
97
97
|
<https://github.com/heindewilde/yungle-clients> · MIT
|
package/dist/index.mjs
CHANGED
|
@@ -121,7 +121,7 @@ async function fetchTo(url, dest, size, doFetch) {
|
|
|
121
121
|
}
|
|
122
122
|
|
|
123
123
|
// src/server.ts
|
|
124
|
-
var MCP_VERSION = true ? "0.3.
|
|
124
|
+
var MCP_VERSION = true ? "0.3.2" : "dev";
|
|
125
125
|
function createServer(client, opts = { canWrite: false, canEmail: false, local: false }) {
|
|
126
126
|
const server = new McpServer(
|
|
127
127
|
{ name: "yungle", version: MCP_VERSION },
|
|
@@ -359,60 +359,62 @@ function createServer(client, opts = { canWrite: false, canEmail: false, local:
|
|
|
359
359
|
}
|
|
360
360
|
);
|
|
361
361
|
}
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
z.
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
362
|
+
if (opts.canWrite) {
|
|
363
|
+
server.registerTool(
|
|
364
|
+
"create_transfer",
|
|
365
|
+
{
|
|
366
|
+
title: "Prepare a transfer",
|
|
367
|
+
description: [
|
|
368
|
+
"Prepare a draft transfer for files that live on a machine with a shell, and return one",
|
|
369
|
+
"upload command per file. Each command (`npx -y yungle-cli put \u2026`) needs no Yungle key: it",
|
|
370
|
+
"carries a token that can write only that file, for two hours, and it resumes if interrupted.",
|
|
371
|
+
"Nothing is shared and nobody is emailed until the transfer is finalized."
|
|
372
|
+
].join("\n"),
|
|
373
|
+
inputSchema: {
|
|
374
|
+
files: z.array(
|
|
375
|
+
z.object({
|
|
376
|
+
name: z.string(),
|
|
377
|
+
size: z.number().int().nonnegative().describe("Exact byte size."),
|
|
378
|
+
path: z.string().optional().describe("Folder within the upload."),
|
|
379
|
+
localPath: z.string().optional().describe("Where the file is on the machine that will run the command.")
|
|
380
|
+
})
|
|
381
|
+
).min(1).max(500),
|
|
382
|
+
title: z.string().optional().describe("Label for the dashboard; never shown to recipients."),
|
|
383
|
+
expiresInDays: z.number().int().positive().optional()
|
|
384
|
+
},
|
|
385
|
+
annotations: { title: "Prepare a transfer", readOnlyHint: false, destructiveHint: false, idempotentHint: false }
|
|
383
386
|
},
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
return fail(errorText(err));
|
|
387
|
+
async ({ files, title, expiresInDays }) => {
|
|
388
|
+
try {
|
|
389
|
+
const created = await client.createTransfer({
|
|
390
|
+
files: files.map(({ localPath: _local, ...f }) => f),
|
|
391
|
+
title,
|
|
392
|
+
expiresInDays
|
|
393
|
+
});
|
|
394
|
+
return ok({
|
|
395
|
+
transferId: created.transfer.id,
|
|
396
|
+
// Said in the payload, not only the description: the payload is what a
|
|
397
|
+
// model summarises back to the user.
|
|
398
|
+
status: "draft \u2014 nothing uploaded yet, nobody emailed",
|
|
399
|
+
uploadCommands: created.files.map((t, i) => ({
|
|
400
|
+
name: t.name,
|
|
401
|
+
command: `npx -y yungle-cli@latest put ${shellQuote(files[i].localPath ?? `./${t.name}`)} --target ${packTarget({
|
|
402
|
+
e: created.tusEndpoint,
|
|
403
|
+
i: t.id,
|
|
404
|
+
t: t.uploadToken,
|
|
405
|
+
n: t.name,
|
|
406
|
+
s: t.size
|
|
407
|
+
})}`
|
|
408
|
+
})),
|
|
409
|
+
next: "Run each command where the file is. When they have finished, finalize the transfer to get its link \u2014 or upload in the Yungle dashboard instead.",
|
|
410
|
+
expiresAt: created.transfer.expiresAt
|
|
411
|
+
});
|
|
412
|
+
} catch (err) {
|
|
413
|
+
return fail(errorText(err));
|
|
414
|
+
}
|
|
413
415
|
}
|
|
414
|
-
|
|
415
|
-
|
|
416
|
+
);
|
|
417
|
+
}
|
|
416
418
|
if (opts.canWrite) {
|
|
417
419
|
server.registerTool(
|
|
418
420
|
"finalize_transfer",
|
package/dist/server.mjs
CHANGED
|
@@ -222,7 +222,7 @@ function pageQuery(page, extra = {}) {
|
|
|
222
222
|
const q = params.toString();
|
|
223
223
|
return q ? `?${q}` : "";
|
|
224
224
|
}
|
|
225
|
-
var CLIENT_VERSION = true ? "0.3.
|
|
225
|
+
var CLIENT_VERSION = true ? "0.3.2" : "dev";
|
|
226
226
|
var YungleClient = class {
|
|
227
227
|
baseUrl;
|
|
228
228
|
apiKey;
|
|
@@ -262,6 +262,10 @@ var YungleClient = class {
|
|
|
262
262
|
me() {
|
|
263
263
|
return this.request("GET", "/me");
|
|
264
264
|
}
|
|
265
|
+
/** Your invite code and link, and the free months it has earned. */
|
|
266
|
+
getReferral() {
|
|
267
|
+
return this.request("GET", "/me/referral");
|
|
268
|
+
}
|
|
265
269
|
// ── Transfers ─────────────────────────────────────────────────────────────
|
|
266
270
|
/**
|
|
267
271
|
* One page of transfers, newest first — 100 by default, up to 500. Pass the
|
|
@@ -506,22 +510,31 @@ var YungleClient = class {
|
|
|
506
510
|
const safe = method === "GET" || method === "DELETE" || idempotencyKey !== void 0 || opts?.idempotent === true;
|
|
507
511
|
let attempt = 0;
|
|
508
512
|
for (; ; ) {
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
513
|
+
let res;
|
|
514
|
+
try {
|
|
515
|
+
res = await this.doFetch(`${this.baseUrl}${path}`, {
|
|
516
|
+
method,
|
|
517
|
+
headers: {
|
|
518
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
519
|
+
Accept: "application/json",
|
|
520
|
+
"User-Agent": this.userAgent,
|
|
521
|
+
...idempotencyKey ? { "Idempotency-Key": idempotencyKey } : {},
|
|
522
|
+
...body === void 0 ? {} : { "Content-Type": "application/json" }
|
|
523
|
+
},
|
|
524
|
+
...body === void 0 ? {} : { body: JSON.stringify(body) }
|
|
525
|
+
});
|
|
526
|
+
} catch (err) {
|
|
527
|
+
const aborted = err instanceof Error && (err.name === "AbortError" || err.name === "TimeoutError");
|
|
528
|
+
if (aborted || !safe || attempt >= this.maxRetries) throw err;
|
|
529
|
+
await sleep(backoffMs(attempt, null));
|
|
530
|
+
attempt++;
|
|
531
|
+
continue;
|
|
532
|
+
}
|
|
520
533
|
if (res.ok) return await res.json();
|
|
521
534
|
const error = await toError(res);
|
|
522
535
|
const mayRetry = error.status === 429 || safe && error.retryable;
|
|
523
536
|
if (!mayRetry || attempt >= this.maxRetries) throw error;
|
|
524
|
-
await sleep(backoffMs(attempt, error));
|
|
537
|
+
await sleep(backoffMs(attempt, error.retryAfterSeconds));
|
|
525
538
|
attempt++;
|
|
526
539
|
}
|
|
527
540
|
}
|
|
@@ -557,8 +570,7 @@ function parseRetryAfter(value, now = Date.now()) {
|
|
|
557
570
|
const seconds = Math.ceil((at - now) / 1e3);
|
|
558
571
|
return seconds >= 0 ? seconds : null;
|
|
559
572
|
}
|
|
560
|
-
function backoffMs(attempt,
|
|
561
|
-
const stated = error.retryAfterSeconds;
|
|
573
|
+
function backoffMs(attempt, stated) {
|
|
562
574
|
if (stated !== null) return stated * 1e3;
|
|
563
575
|
return Math.min(2 ** attempt * 500, 8e3) + Math.floor(Math.random() * 250);
|
|
564
576
|
}
|
|
@@ -686,7 +698,7 @@ async function fetchTo(url, dest, size, doFetch) {
|
|
|
686
698
|
}
|
|
687
699
|
|
|
688
700
|
// src/server.ts
|
|
689
|
-
var MCP_VERSION = true ? "0.3.
|
|
701
|
+
var MCP_VERSION = true ? "0.3.2" : "dev";
|
|
690
702
|
function createServer(client, opts = { canWrite: false, canEmail: false, local: false }) {
|
|
691
703
|
const server = new McpServer(
|
|
692
704
|
{ name: "yungle", version: MCP_VERSION },
|
|
@@ -924,60 +936,62 @@ function createServer(client, opts = { canWrite: false, canEmail: false, local:
|
|
|
924
936
|
}
|
|
925
937
|
);
|
|
926
938
|
}
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
z.
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
939
|
+
if (opts.canWrite) {
|
|
940
|
+
server.registerTool(
|
|
941
|
+
"create_transfer",
|
|
942
|
+
{
|
|
943
|
+
title: "Prepare a transfer",
|
|
944
|
+
description: [
|
|
945
|
+
"Prepare a draft transfer for files that live on a machine with a shell, and return one",
|
|
946
|
+
"upload command per file. Each command (`npx -y yungle-cli put \u2026`) needs no Yungle key: it",
|
|
947
|
+
"carries a token that can write only that file, for two hours, and it resumes if interrupted.",
|
|
948
|
+
"Nothing is shared and nobody is emailed until the transfer is finalized."
|
|
949
|
+
].join("\n"),
|
|
950
|
+
inputSchema: {
|
|
951
|
+
files: z.array(
|
|
952
|
+
z.object({
|
|
953
|
+
name: z.string(),
|
|
954
|
+
size: z.number().int().nonnegative().describe("Exact byte size."),
|
|
955
|
+
path: z.string().optional().describe("Folder within the upload."),
|
|
956
|
+
localPath: z.string().optional().describe("Where the file is on the machine that will run the command.")
|
|
957
|
+
})
|
|
958
|
+
).min(1).max(500),
|
|
959
|
+
title: z.string().optional().describe("Label for the dashboard; never shown to recipients."),
|
|
960
|
+
expiresInDays: z.number().int().positive().optional()
|
|
961
|
+
},
|
|
962
|
+
annotations: { title: "Prepare a transfer", readOnlyHint: false, destructiveHint: false, idempotentHint: false }
|
|
948
963
|
},
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
return fail(errorText(err));
|
|
964
|
+
async ({ files, title, expiresInDays }) => {
|
|
965
|
+
try {
|
|
966
|
+
const created = await client.createTransfer({
|
|
967
|
+
files: files.map(({ localPath: _local, ...f }) => f),
|
|
968
|
+
title,
|
|
969
|
+
expiresInDays
|
|
970
|
+
});
|
|
971
|
+
return ok({
|
|
972
|
+
transferId: created.transfer.id,
|
|
973
|
+
// Said in the payload, not only the description: the payload is what a
|
|
974
|
+
// model summarises back to the user.
|
|
975
|
+
status: "draft \u2014 nothing uploaded yet, nobody emailed",
|
|
976
|
+
uploadCommands: created.files.map((t, i) => ({
|
|
977
|
+
name: t.name,
|
|
978
|
+
command: `npx -y yungle-cli@latest put ${shellQuote(files[i].localPath ?? `./${t.name}`)} --target ${packTarget({
|
|
979
|
+
e: created.tusEndpoint,
|
|
980
|
+
i: t.id,
|
|
981
|
+
t: t.uploadToken,
|
|
982
|
+
n: t.name,
|
|
983
|
+
s: t.size
|
|
984
|
+
})}`
|
|
985
|
+
})),
|
|
986
|
+
next: "Run each command where the file is. When they have finished, finalize the transfer to get its link \u2014 or upload in the Yungle dashboard instead.",
|
|
987
|
+
expiresAt: created.transfer.expiresAt
|
|
988
|
+
});
|
|
989
|
+
} catch (err) {
|
|
990
|
+
return fail(errorText(err));
|
|
991
|
+
}
|
|
978
992
|
}
|
|
979
|
-
|
|
980
|
-
|
|
993
|
+
);
|
|
994
|
+
}
|
|
981
995
|
if (opts.canWrite) {
|
|
982
996
|
server.registerTool(
|
|
983
997
|
"finalize_transfer",
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "yungle-mcp",
|
|
3
3
|
"mcpName": "co.yungle/yungle",
|
|
4
|
-
"version": "0.3.
|
|
4
|
+
"version": "0.3.2",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "MCP server for Yungle: let Claude, Cursor or any MCP client read your file transfers, share files and send them, with your approval.",
|
|
7
7
|
"bin": {
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"dependencies": {
|
|
16
16
|
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
17
17
|
"zod": "^3.24.1",
|
|
18
|
-
"yungle-client": "^0.3.
|
|
18
|
+
"yungle-client": "^0.3.2"
|
|
19
19
|
},
|
|
20
20
|
"devDependencies": {
|
|
21
21
|
"@types/node": "^22.10.5",
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"type": "git",
|
|
44
44
|
"url": "git+https://github.com/heindewilde/yungle-clients.git"
|
|
45
45
|
},
|
|
46
|
-
"homepage": "https://yungle.co/
|
|
46
|
+
"homepage": "https://docs.yungle.co/mcp-server?ref=npm",
|
|
47
47
|
"bugs": {
|
|
48
48
|
"url": "https://github.com/heindewilde/yungle-clients/issues"
|
|
49
49
|
},
|