vessels-mcp 0.2.0 → 0.4.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.
Files changed (2) hide show
  1. package/dist/index.js +87 -7
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -3,7 +3,7 @@
3
3
  // src/index.ts
4
4
  import { readFileSync } from "fs";
5
5
  import { homedir } from "os";
6
- import { join } from "path";
6
+ import { join, basename } from "path";
7
7
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
8
8
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
9
9
  import { z } from "zod";
@@ -11,15 +11,22 @@ import { Vessels } from "vessels-sdk";
11
11
  function resolveKey() {
12
12
  const env = process.env.VESSELS_API_KEY;
13
13
  if (env && !env.startsWith("${")) return env;
14
- try {
15
- const k = readFileSync(join(homedir(), ".thru-do-vessels-key"), "utf8").trim();
16
- if (k) return k;
17
- } catch {
14
+ for (const p of [
15
+ join(homedir(), ".config", "vessels-mcp", "key"),
16
+ join(homedir(), ".thru-do-vessels-key")
17
+ // legacy path — kept for back-compat
18
+ ]) {
19
+ try {
20
+ const k = readFileSync(p, "utf8").trim();
21
+ if (k) return k;
22
+ } catch {
23
+ }
18
24
  }
19
25
  return void 0;
20
26
  }
21
27
  var apiKey = resolveKey();
22
- var defaultVessel = process.env.VESSELS_VESSEL ?? "claude-code";
28
+ var cwdName = basename(process.cwd()) || "workspace";
29
+ var defaultVessel = process.env.VESSELS_VESSEL ?? `claude-code-${cwdName}`;
23
30
  var vesselTitle = process.env.VESSELS_VESSEL_TITLE ?? "Claude Code";
24
31
  var baseUrl = process.env.VESSELS_BASE_URL ?? "https://vessels.app";
25
32
  if (!apiKey) {
@@ -68,7 +75,7 @@ async function ask(vessel, prompt, build, expectedType) {
68
75
  if (!r.hasMore) await sleep(2e3);
69
76
  }
70
77
  }
71
- var server = new McpServer({ name: "vessels", version: "0.2.0" });
78
+ var server = new McpServer({ name: "vessels", version: "0.4.0" });
72
79
  server.registerTool(
73
80
  "list_vessels",
74
81
  {
@@ -305,5 +312,78 @@ server.registerTool(
305
312
  return ok("ok");
306
313
  }
307
314
  );
315
+ server.registerTool(
316
+ "update_vessel",
317
+ {
318
+ title: "Update a vessel",
319
+ description: "Edit vessel-level state WITHOUT posting a message: title, labels (replaces the whole set), status, archived, or pinned (pin to top of the list). Use list_vessels to find the external_id.",
320
+ inputSchema: {
321
+ vessel: z.string().describe("Vessel external_id."),
322
+ title: z.string().optional(),
323
+ labels: z.array(z.string()).optional().describe("Replace the vessel labels (full set)."),
324
+ status: z.enum(["active", "waiting", "resolved"]).optional(),
325
+ archived: z.boolean().optional().describe("Archive (true) or unarchive (false)."),
326
+ pinned: z.boolean().optional().describe("Pin (true) / unpin (false) to the top of the list.")
327
+ }
328
+ },
329
+ async ({ vessel, title, labels, status, archived, pinned }) => {
330
+ const body = {
331
+ ...title !== void 0 ? { title } : {},
332
+ ...labels !== void 0 ? { labels } : {},
333
+ ...status !== void 0 ? { vesselStatus: status } : {},
334
+ ...archived !== void 0 ? { archived } : {},
335
+ ...pinned !== void 0 ? { pinned } : {}
336
+ };
337
+ if (!Object.keys(body).length) return ok("error: nothing to update");
338
+ const res = await fetch(`${baseUrl}/api/v1/vessels/by-external/${encodeURIComponent(vessel)}`, {
339
+ method: "PATCH",
340
+ headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
341
+ body: JSON.stringify(body)
342
+ });
343
+ const data = await res.json();
344
+ return ok(res.ok ? "ok" : `error: ${data.error ?? res.status}`);
345
+ }
346
+ );
347
+ server.registerTool(
348
+ "delete_vessel",
349
+ {
350
+ title: "Delete a vessel (permanent)",
351
+ description: "Permanently delete a vessel and all its messages (cascade). IRREVERSIBLE \u2014 prefer update_vessel with archived:true unless you truly mean to destroy it.",
352
+ inputSchema: { vessel: z.string().describe("Vessel external_id to delete.") }
353
+ },
354
+ async ({ vessel }) => {
355
+ const res = await fetch(`${baseUrl}/api/v1/vessels/by-external/${encodeURIComponent(vessel)}`, {
356
+ method: "DELETE",
357
+ headers: { Authorization: `Bearer ${apiKey}` }
358
+ });
359
+ const data = await res.json();
360
+ return ok(res.ok ? `deleted ${vessel}` : `error: ${data.error ?? res.status}`);
361
+ }
362
+ );
363
+ server.registerTool(
364
+ "clear_messages",
365
+ {
366
+ title: "Clear a range of messages (rewind)",
367
+ description: `Trim a vessel's feed back to a point \u2014 the durable op behind a "/rewind". Pass EXACTLY ONE anchor; the anchor row is kept. "afterMessageId"/"after" deletes everything strictly newer ("rewind to here"); "beforeMessageId"/"before" deletes everything strictly older. "source" scopes it: "agent" (default) = agent+system, "user" = the human's messages, "all" = everything. This trims the HUMAN-VISIBLE feed only \u2014 it does NOT clear your agent's own context. Hard delete, irreversible \u2014 use dryRun:true first to preview the count.`,
368
+ inputSchema: {
369
+ vessel: z.string().optional().describe(`Vessel external_id (default "${defaultVessel}").`),
370
+ afterMessageId: z.string().optional().describe('Delete everything after this message id ("rewind to here").'),
371
+ beforeMessageId: z.string().optional().describe("Delete everything before this message id."),
372
+ after: z.string().optional().describe("Delete messages strictly newer than this ISO timestamp."),
373
+ before: z.string().optional().describe("Delete messages strictly older than this ISO timestamp."),
374
+ source: z.enum(["agent", "user", "all"]).optional().describe('Whose messages may go (default "agent" = agent+system).'),
375
+ dryRun: z.boolean().optional().describe("Count what would be deleted without deleting.")
376
+ }
377
+ },
378
+ async ({ vessel, afterMessageId, beforeMessageId, after, before, source, dryRun }) => {
379
+ const tgt = vessel ?? defaultVessel;
380
+ try {
381
+ const r = await v.clearMessages(tgt, { afterMessageId, beforeMessageId, after, before, source, dryRun });
382
+ return ok(r.dryRun ? `would delete ${r.deleted} message(s)` : `deleted ${r.deleted} message(s)`);
383
+ } catch (e) {
384
+ return ok(`error: ${e instanceof Error ? e.message : String(e)}`);
385
+ }
386
+ }
387
+ );
308
388
  await server.connect(new StdioServerTransport());
309
389
  console.error(`[vessels-mcp] ready \u2014 default vessel "${defaultVessel}" @ ${baseUrl}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vessels-mcp",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "description": "Vessels MCP server — give any MCP client (Claude Code, Cursor, …) a tool to message a human and BLOCK until they answer.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -20,7 +20,7 @@
20
20
  "dependencies": {
21
21
  "@modelcontextprotocol/sdk": "^1.0.0",
22
22
  "zod": "^3.22",
23
- "vessels-sdk": "0.16.0"
23
+ "vessels-sdk": "^0.19.0"
24
24
  },
25
25
  "devDependencies": {
26
26
  "tsup": "^8.5.1",