yungle-mcp 0.2.0 → 0.2.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/dist/index.mjs CHANGED
@@ -74,7 +74,7 @@ async function uploadPath(endpoint, target, path, size, doFetch = fetch) {
74
74
  }
75
75
 
76
76
  // src/server.ts
77
- var MCP_VERSION = true ? "0.2.0" : "dev";
77
+ var MCP_VERSION = true ? "0.2.2" : "dev";
78
78
  function createServer(client, opts = { canWrite: false, canEmail: false, local: false }) {
79
79
  const server = new McpServer(
80
80
  { name: "yungle", version: MCP_VERSION },
@@ -95,7 +95,7 @@ function createServer(client, opts = { canWrite: false, canEmail: false, local:
95
95
  title: "Account and storage",
96
96
  description: "Answers: how much storage am I using? how much is left? what plan am I on? what can this connection do? Returns the workspace, its plan and quota, and the key's permissions.",
97
97
  inputSchema: {},
98
- annotations: { readOnlyHint: true }
98
+ annotations: { title: "Account and storage", readOnlyHint: true }
99
99
  },
100
100
  async () => ok(await client.me())
101
101
  );
@@ -105,7 +105,7 @@ function createServer(client, opts = { canWrite: false, canEmail: false, local:
105
105
  title: "List transfers",
106
106
  description: "Answers: what have I sent recently? what is expiring soon? which deliveries has nobody picked up? who did I send that to? Returns recent transfers with size, recipients, download count, expiry date and share link. Start here for any question about sent files.",
107
107
  inputSchema: {},
108
- annotations: { readOnlyHint: true }
108
+ annotations: { title: "List transfers", readOnlyHint: true }
109
109
  },
110
110
  async () => {
111
111
  const { transfers } = await client.listTransfers();
@@ -118,7 +118,7 @@ function createServer(client, opts = { canWrite: false, canEmail: false, local:
118
118
  title: "Transfer detail",
119
119
  description: "Answers: what is in this transfer? was it delivered? has a specific recipient opened it? is it safe to share? Returns the files with their malware-scan verdicts, plus per-recipient delivery and download status. Needs an id from list_transfers.",
120
120
  inputSchema: { id: z.string().describe("Transfer id from list_transfers.") },
121
- annotations: { readOnlyHint: true }
121
+ annotations: { title: "Transfer detail", readOnlyHint: true }
122
122
  },
123
123
  async ({ id }) => ok(wrapUntrusted(await client.getTransfer(id)))
124
124
  );
@@ -128,7 +128,7 @@ function createServer(client, opts = { canWrite: false, canEmail: false, local:
128
128
  title: "Download receipts",
129
129
  description: "Answers: did the client download it? when? how many times? Returns download events with timestamps and per-recipient status. IMPORTANT when counting: one page visit is one download, not one per file \u2014 events sharing a sessionId are a single visit, so count distinct sessions or a single visitor looks like eight.",
130
130
  inputSchema: { id: z.string() },
131
- annotations: { readOnlyHint: true }
131
+ annotations: { title: "Download receipts", readOnlyHint: true }
132
132
  },
133
133
  async ({ id }) => ok(wrapUntrusted(await client.transferDownloads(id)))
134
134
  );
@@ -138,7 +138,7 @@ function createServer(client, opts = { canWrite: false, canEmail: false, local:
138
138
  title: "List collections",
139
139
  description: "Answers: what collections do I have? which are empty? how big is each one? Returns collections with file counts, sizes and when each was last touched. A collection is a durable space clients are invited into, as opposed to a one-off transfer. Never includes the vault, which is unreadable to this server by design.",
140
140
  inputSchema: {},
141
- annotations: { readOnlyHint: true }
141
+ annotations: { title: "List collections", readOnlyHint: true }
142
142
  },
143
143
  async () => ok(wrapUntrusted(await client.listCollections()))
144
144
  );
@@ -148,7 +148,7 @@ function createServer(client, opts = { canWrite: false, canEmail: false, local:
148
148
  title: "Collection detail",
149
149
  description: "Answers: what is the link for this collection? how much is in it? when does it expire? Returns one collection with its secret share link, file count, total size and expiry. Needs an id from list_collections.",
150
150
  inputSchema: { id: z.string() },
151
- annotations: { readOnlyHint: true }
151
+ annotations: { title: "Collection detail", readOnlyHint: true }
152
152
  },
153
153
  async ({ id }) => ok(wrapUntrusted(await client.getCollection(id)))
154
154
  );
@@ -158,7 +158,7 @@ function createServer(client, opts = { canWrite: false, canEmail: false, local:
158
158
  title: "Files in a collection",
159
159
  description: 'Answers: what files are in this collection? what is in this folder? are the raws uploaded yet? Returns filenames, sizes and types. Omit folderId for every file; pass "root" for the top level only, or a folder id from list_folders. Filenames only \u2014 never file contents.',
160
160
  inputSchema: { id: z.string(), folderId: z.string().optional() },
161
- annotations: { readOnlyHint: true }
161
+ annotations: { title: "Files in a collection", readOnlyHint: true }
162
162
  },
163
163
  async ({ id, folderId }) => ok(wrapUntrusted(await client.listCollectionFiles(id, folderId)))
164
164
  );
@@ -168,7 +168,7 @@ function createServer(client, opts = { canWrite: false, canEmail: false, local:
168
168
  title: "Folders in a collection",
169
169
  description: "Answers: how is this collection organised? what folders exist? Returns the folder tree. Ordered by depth then path, which is NOT a pre-order traversal \u2014 build the tree from parentId rather than trusting the order. Each folder also carries its full materialised path.",
170
170
  inputSchema: { id: z.string() },
171
- annotations: { readOnlyHint: true }
171
+ annotations: { title: "Folders in a collection", readOnlyHint: true }
172
172
  },
173
173
  async ({ id }) => ok(wrapUntrusted(await client.listFolders(id)))
174
174
  );
@@ -178,7 +178,7 @@ function createServer(client, opts = { canWrite: false, canEmail: false, local:
178
178
  title: "Guests on a collection",
179
179
  description: "Answers: who has access to this collection? who did I invite? has someone accepted? Returns the invited guests and their status. Guests are not workspace members \u2014 they can view and download this one collection and nothing else.",
180
180
  inputSchema: { id: z.string() },
181
- annotations: { readOnlyHint: true }
181
+ annotations: { title: "Guests on a collection", readOnlyHint: true }
182
182
  },
183
183
  async ({ id }) => ok(wrapUntrusted(await client.listGuests(id)))
184
184
  );
@@ -188,7 +188,7 @@ function createServer(client, opts = { canWrite: false, canEmail: false, local:
188
188
  title: "Address book",
189
189
  description: "Answers: what is this client's email address? who do I have saved? Returns the workspace address book. A contact grants no access on its own \u2014 it is a convenience, not a permission.",
190
190
  inputSchema: {},
191
- annotations: { readOnlyHint: true }
191
+ annotations: { title: "Address book", readOnlyHint: true }
192
192
  },
193
193
  async () => ok(wrapUntrusted(await client.listContacts()))
194
194
  );
@@ -214,7 +214,7 @@ function createServer(client, opts = { canWrite: false, canEmail: false, local:
214
214
  title: z.string().optional().describe("Label for the dashboard; never shown to recipients."),
215
215
  expiresInDays: z.number().int().positive().optional()
216
216
  },
217
- annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false }
217
+ annotations: { title: "Prepare a transfer", readOnlyHint: false, destructiveHint: false, idempotentHint: false }
218
218
  },
219
219
  async ({ files, title, expiresInDays }) => {
220
220
  const created = await client.createTransfer({ files, title, expiresInDays });
@@ -250,7 +250,7 @@ function createServer(client, opts = { canWrite: false, canEmail: false, local:
250
250
  title: z.string().optional().describe("Label for the dashboard; never shown to recipients."),
251
251
  expiresInDays: z.number().int().positive().optional()
252
252
  },
253
- annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }
253
+ annotations: { title: "Share content as a link", readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }
254
254
  },
255
255
  async ({ files, title, expiresInDays }) => {
256
256
  const contents = files.map(
@@ -284,7 +284,7 @@ function createServer(client, opts = { canWrite: false, canEmail: false, local:
284
284
  title: z.string().optional(),
285
285
  expiresInDays: z.number().int().positive().optional()
286
286
  },
287
- annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }
287
+ annotations: { title: "Share files from this computer as a link", readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }
288
288
  },
289
289
  async ({ paths: asked, title, expiresInDays }, extra) => {
290
290
  const isHidden = (p) => p.split(/[\\/]/).some((seg) => seg.startsWith(".") && seg !== "." && seg !== "..");
@@ -332,7 +332,7 @@ function createServer(client, opts = { canWrite: false, canEmail: false, local:
332
332
  recipients: z.array(z.string().email()).min(1).max(10),
333
333
  message: z.string().max(2e3).optional().describe("A note shown in the email.")
334
334
  },
335
- annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true }
335
+ annotations: { title: "Email a transfer to recipients", readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true }
336
336
  },
337
337
  async ({ transferId, recipients, message }, extra) => {
338
338
  const { transfer } = await client.getTransfer(transferId);
@@ -369,9 +369,11 @@ async function startStdioServer() {
369
369
  const apiKey = process.env.YUNGLE_API_KEY?.trim();
370
370
  if (!apiKey) {
371
371
  process.stderr.write(
372
- "YUNGLE_API_KEY is not set. Create a key at https://yungle.co/dashboard/settings/api\n"
372
+ "YUNGLE_API_KEY is not set: listing tools only. Create a key at https://yungle.co/dashboard/settings/api\n"
373
373
  );
374
- process.exit(2);
374
+ const server2 = createServer(keylessClient(), { canWrite: false, canEmail: false, local: true });
375
+ await server2.connect(new StdioServerTransport());
376
+ return;
375
377
  }
376
378
  const client = new YungleClient({
377
379
  apiKey,
@@ -397,6 +399,12 @@ async function startStdioServer() {
397
399
  });
398
400
  await server.connect(new StdioServerTransport());
399
401
  }
402
+ function keylessClient() {
403
+ const message = "YUNGLE_API_KEY is not set, so this server cannot reach Yungle. Create a key at https://yungle.co/dashboard/settings/api and add it to this MCP server's environment, or connect to the hosted server at https://yungle.co/mcp instead.";
404
+ return new Proxy({}, {
405
+ get: () => () => Promise.reject(new Error(message))
406
+ });
407
+ }
400
408
  function ok(payload) {
401
409
  return { content: [{ type: "text", text: JSON.stringify(payload, null, 2) }] };
402
410
  }
@@ -406,5 +414,6 @@ function fail(message) {
406
414
  export {
407
415
  MCP_VERSION,
408
416
  createServer,
417
+ keylessClient,
409
418
  startStdioServer
410
419
  };
package/dist/server.d.ts CHANGED
@@ -23,3 +23,5 @@ export declare function createServer(client: YungleClient, opts?: ServerOptions)
23
23
  * emails nobody and needs only transfers:write.
24
24
  */
25
25
  export declare function startStdioServer(): Promise<void>;
26
+ /** A client whose every call explains that there is no key. */
27
+ export declare function keylessClient(): YungleClient;
package/dist/server.mjs CHANGED
@@ -345,7 +345,7 @@ async function uploadPath(endpoint, target, path, size, doFetch = fetch) {
345
345
  }
346
346
 
347
347
  // src/server.ts
348
- var MCP_VERSION = true ? "0.2.0" : "dev";
348
+ var MCP_VERSION = true ? "0.2.2" : "dev";
349
349
  function createServer(client, opts = { canWrite: false, canEmail: false, local: false }) {
350
350
  const server = new McpServer(
351
351
  { name: "yungle", version: MCP_VERSION },
@@ -366,7 +366,7 @@ function createServer(client, opts = { canWrite: false, canEmail: false, local:
366
366
  title: "Account and storage",
367
367
  description: "Answers: how much storage am I using? how much is left? what plan am I on? what can this connection do? Returns the workspace, its plan and quota, and the key's permissions.",
368
368
  inputSchema: {},
369
- annotations: { readOnlyHint: true }
369
+ annotations: { title: "Account and storage", readOnlyHint: true }
370
370
  },
371
371
  async () => ok(await client.me())
372
372
  );
@@ -376,7 +376,7 @@ function createServer(client, opts = { canWrite: false, canEmail: false, local:
376
376
  title: "List transfers",
377
377
  description: "Answers: what have I sent recently? what is expiring soon? which deliveries has nobody picked up? who did I send that to? Returns recent transfers with size, recipients, download count, expiry date and share link. Start here for any question about sent files.",
378
378
  inputSchema: {},
379
- annotations: { readOnlyHint: true }
379
+ annotations: { title: "List transfers", readOnlyHint: true }
380
380
  },
381
381
  async () => {
382
382
  const { transfers } = await client.listTransfers();
@@ -389,7 +389,7 @@ function createServer(client, opts = { canWrite: false, canEmail: false, local:
389
389
  title: "Transfer detail",
390
390
  description: "Answers: what is in this transfer? was it delivered? has a specific recipient opened it? is it safe to share? Returns the files with their malware-scan verdicts, plus per-recipient delivery and download status. Needs an id from list_transfers.",
391
391
  inputSchema: { id: z.string().describe("Transfer id from list_transfers.") },
392
- annotations: { readOnlyHint: true }
392
+ annotations: { title: "Transfer detail", readOnlyHint: true }
393
393
  },
394
394
  async ({ id }) => ok(wrapUntrusted(await client.getTransfer(id)))
395
395
  );
@@ -399,7 +399,7 @@ function createServer(client, opts = { canWrite: false, canEmail: false, local:
399
399
  title: "Download receipts",
400
400
  description: "Answers: did the client download it? when? how many times? Returns download events with timestamps and per-recipient status. IMPORTANT when counting: one page visit is one download, not one per file \u2014 events sharing a sessionId are a single visit, so count distinct sessions or a single visitor looks like eight.",
401
401
  inputSchema: { id: z.string() },
402
- annotations: { readOnlyHint: true }
402
+ annotations: { title: "Download receipts", readOnlyHint: true }
403
403
  },
404
404
  async ({ id }) => ok(wrapUntrusted(await client.transferDownloads(id)))
405
405
  );
@@ -409,7 +409,7 @@ function createServer(client, opts = { canWrite: false, canEmail: false, local:
409
409
  title: "List collections",
410
410
  description: "Answers: what collections do I have? which are empty? how big is each one? Returns collections with file counts, sizes and when each was last touched. A collection is a durable space clients are invited into, as opposed to a one-off transfer. Never includes the vault, which is unreadable to this server by design.",
411
411
  inputSchema: {},
412
- annotations: { readOnlyHint: true }
412
+ annotations: { title: "List collections", readOnlyHint: true }
413
413
  },
414
414
  async () => ok(wrapUntrusted(await client.listCollections()))
415
415
  );
@@ -419,7 +419,7 @@ function createServer(client, opts = { canWrite: false, canEmail: false, local:
419
419
  title: "Collection detail",
420
420
  description: "Answers: what is the link for this collection? how much is in it? when does it expire? Returns one collection with its secret share link, file count, total size and expiry. Needs an id from list_collections.",
421
421
  inputSchema: { id: z.string() },
422
- annotations: { readOnlyHint: true }
422
+ annotations: { title: "Collection detail", readOnlyHint: true }
423
423
  },
424
424
  async ({ id }) => ok(wrapUntrusted(await client.getCollection(id)))
425
425
  );
@@ -429,7 +429,7 @@ function createServer(client, opts = { canWrite: false, canEmail: false, local:
429
429
  title: "Files in a collection",
430
430
  description: 'Answers: what files are in this collection? what is in this folder? are the raws uploaded yet? Returns filenames, sizes and types. Omit folderId for every file; pass "root" for the top level only, or a folder id from list_folders. Filenames only \u2014 never file contents.',
431
431
  inputSchema: { id: z.string(), folderId: z.string().optional() },
432
- annotations: { readOnlyHint: true }
432
+ annotations: { title: "Files in a collection", readOnlyHint: true }
433
433
  },
434
434
  async ({ id, folderId }) => ok(wrapUntrusted(await client.listCollectionFiles(id, folderId)))
435
435
  );
@@ -439,7 +439,7 @@ function createServer(client, opts = { canWrite: false, canEmail: false, local:
439
439
  title: "Folders in a collection",
440
440
  description: "Answers: how is this collection organised? what folders exist? Returns the folder tree. Ordered by depth then path, which is NOT a pre-order traversal \u2014 build the tree from parentId rather than trusting the order. Each folder also carries its full materialised path.",
441
441
  inputSchema: { id: z.string() },
442
- annotations: { readOnlyHint: true }
442
+ annotations: { title: "Folders in a collection", readOnlyHint: true }
443
443
  },
444
444
  async ({ id }) => ok(wrapUntrusted(await client.listFolders(id)))
445
445
  );
@@ -449,7 +449,7 @@ function createServer(client, opts = { canWrite: false, canEmail: false, local:
449
449
  title: "Guests on a collection",
450
450
  description: "Answers: who has access to this collection? who did I invite? has someone accepted? Returns the invited guests and their status. Guests are not workspace members \u2014 they can view and download this one collection and nothing else.",
451
451
  inputSchema: { id: z.string() },
452
- annotations: { readOnlyHint: true }
452
+ annotations: { title: "Guests on a collection", readOnlyHint: true }
453
453
  },
454
454
  async ({ id }) => ok(wrapUntrusted(await client.listGuests(id)))
455
455
  );
@@ -459,7 +459,7 @@ function createServer(client, opts = { canWrite: false, canEmail: false, local:
459
459
  title: "Address book",
460
460
  description: "Answers: what is this client's email address? who do I have saved? Returns the workspace address book. A contact grants no access on its own \u2014 it is a convenience, not a permission.",
461
461
  inputSchema: {},
462
- annotations: { readOnlyHint: true }
462
+ annotations: { title: "Address book", readOnlyHint: true }
463
463
  },
464
464
  async () => ok(wrapUntrusted(await client.listContacts()))
465
465
  );
@@ -485,7 +485,7 @@ function createServer(client, opts = { canWrite: false, canEmail: false, local:
485
485
  title: z.string().optional().describe("Label for the dashboard; never shown to recipients."),
486
486
  expiresInDays: z.number().int().positive().optional()
487
487
  },
488
- annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false }
488
+ annotations: { title: "Prepare a transfer", readOnlyHint: false, destructiveHint: false, idempotentHint: false }
489
489
  },
490
490
  async ({ files, title, expiresInDays }) => {
491
491
  const created = await client.createTransfer({ files, title, expiresInDays });
@@ -521,7 +521,7 @@ function createServer(client, opts = { canWrite: false, canEmail: false, local:
521
521
  title: z.string().optional().describe("Label for the dashboard; never shown to recipients."),
522
522
  expiresInDays: z.number().int().positive().optional()
523
523
  },
524
- annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }
524
+ annotations: { title: "Share content as a link", readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }
525
525
  },
526
526
  async ({ files, title, expiresInDays }) => {
527
527
  const contents = files.map(
@@ -555,7 +555,7 @@ function createServer(client, opts = { canWrite: false, canEmail: false, local:
555
555
  title: z.string().optional(),
556
556
  expiresInDays: z.number().int().positive().optional()
557
557
  },
558
- annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }
558
+ annotations: { title: "Share files from this computer as a link", readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }
559
559
  },
560
560
  async ({ paths: asked, title, expiresInDays }, extra) => {
561
561
  const isHidden = (p) => p.split(/[\\/]/).some((seg) => seg.startsWith(".") && seg !== "." && seg !== "..");
@@ -603,7 +603,7 @@ function createServer(client, opts = { canWrite: false, canEmail: false, local:
603
603
  recipients: z.array(z.string().email()).min(1).max(10),
604
604
  message: z.string().max(2e3).optional().describe("A note shown in the email.")
605
605
  },
606
- annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true }
606
+ annotations: { title: "Email a transfer to recipients", readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true }
607
607
  },
608
608
  async ({ transferId, recipients, message }, extra) => {
609
609
  const { transfer } = await client.getTransfer(transferId);
@@ -640,9 +640,11 @@ async function startStdioServer() {
640
640
  const apiKey = process.env.YUNGLE_API_KEY?.trim();
641
641
  if (!apiKey) {
642
642
  process.stderr.write(
643
- "YUNGLE_API_KEY is not set. Create a key at https://yungle.co/dashboard/settings/api\n"
643
+ "YUNGLE_API_KEY is not set: listing tools only. Create a key at https://yungle.co/dashboard/settings/api\n"
644
644
  );
645
- process.exit(2);
645
+ const server2 = createServer(keylessClient(), { canWrite: false, canEmail: false, local: true });
646
+ await server2.connect(new StdioServerTransport());
647
+ return;
646
648
  }
647
649
  const client = new YungleClient({
648
650
  apiKey,
@@ -668,6 +670,12 @@ async function startStdioServer() {
668
670
  });
669
671
  await server.connect(new StdioServerTransport());
670
672
  }
673
+ function keylessClient() {
674
+ const message = "YUNGLE_API_KEY is not set, so this server cannot reach Yungle. Create a key at https://yungle.co/dashboard/settings/api and add it to this MCP server's environment, or connect to the hosted server at https://yungle.co/mcp instead.";
675
+ return new Proxy({}, {
676
+ get: () => () => Promise.reject(new Error(message))
677
+ });
678
+ }
671
679
  function ok(payload) {
672
680
  return { content: [{ type: "text", text: JSON.stringify(payload, null, 2) }] };
673
681
  }
@@ -677,6 +685,7 @@ function fail(message) {
677
685
  export {
678
686
  MCP_VERSION,
679
687
  createServer,
688
+ keylessClient,
680
689
  startStdioServer
681
690
  };
682
691
  await startStdioServer();
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "yungle-mcp",
3
3
  "mcpName": "co.yungle/yungle",
4
- "version": "0.2.0",
4
+ "version": "0.2.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": {