vessels-mcp 0.3.0 → 0.5.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 +91 -27
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -75,7 +75,7 @@ async function ask(vessel, prompt, build, expectedType) {
75
75
  if (!r.hasMore) await sleep(2e3);
76
76
  }
77
77
  }
78
- var server = new McpServer({ name: "vessels", version: "0.2.0" });
78
+ var server = new McpServer({ name: "vessels", version: "0.5.0" });
79
79
  server.registerTool(
80
80
  "list_vessels",
81
81
  {
@@ -87,16 +87,17 @@ server.registerTool(
87
87
  }
88
88
  },
89
89
  async ({ limit, archived }) => {
90
- const res = await fetch(`${baseUrl}/api/v1/vessels?archived=${archived ? "true" : "false"}`, {
91
- headers: { Authorization: `Bearer ${apiKey}` }
92
- });
93
- const data = await res.json();
94
- if (!res.ok) return ok(`error: ${data.error ?? res.status}`);
95
- const rows = (data.vessels ?? []).slice(0, limit ?? 30);
90
+ let vessels;
91
+ try {
92
+ vessels = await v.listVessels({ archived: !!archived });
93
+ } catch (e) {
94
+ return ok(`error: ${e.message}`);
95
+ }
96
+ const rows = vessels.slice(0, limit ?? 30);
96
97
  if (!rows.length) return ok("(no vessels)");
97
98
  const lines = rows.map((x) => {
98
99
  const labels = (x.labels ?? []).length ? ` [${x.labels.join(",")}]` : "";
99
- return `${x.external_id} \xB7${x.status}${x.pinned ? " \xB7pinned" : ""} "${x.title ?? ""}"${labels}`;
100
+ return `${x.externalId} \xB7${x.status}${x.pinned ? " \xB7pinned" : ""} "${x.title ?? ""}"${labels}`;
100
101
  });
101
102
  return ok(lines.join("\n"));
102
103
  }
@@ -142,13 +143,23 @@ server.registerTool(
142
143
  "send",
143
144
  {
144
145
  title: "Send a message or artifact",
145
- description: 'Post a message to a vessel. For a chat reply pass `message`. For a full-width artifact (proposal, report, diff, review) pass `body` (block markdown: headings/lists/tables/quotes) and optionally `title` + `card`. Also supports `pinCard` (persistent header), `labels`, `suggestions` (quick-reply chips), `previewUrl` (a link card), and `status` to set the vessel state. Returns the new message id as "mid:<id>".',
146
+ description: 'Post a message to a vessel. For a chat reply pass `message`. For a full-width artifact (proposal, report, diff, review) pass `body` (block markdown: headings/lists/tables/quotes) and optionally `title` + `card`. Also supports `details` (persistent vessel reference record), `labels`, `suggestions` (quick-reply chips), `previewUrl` (a link card), and `status` to set the vessel state. Returns the new message id as "mid:<id>".',
146
147
  inputSchema: {
147
148
  message: z.string().optional().describe("Chat-bubble text (use this OR body)."),
148
149
  body: z.string().optional().describe("Surface artifact body \u2014 block markdown."),
149
150
  title: z.string().optional().describe("Surface heading."),
150
151
  card: z.object({ title: z.string().optional(), fields: z.array(z.object({ label: z.string(), value: z.string() })) }).optional().describe("Glance-facts card (label/value rows)."),
151
- pinCard: z.object({ title: z.string().optional(), fields: z.array(z.object({ label: z.string(), value: z.string() })) }).optional().describe("Persistent pinned header card for the vessel."),
152
+ details: z.object({
153
+ fields: z.array(
154
+ z.object({
155
+ label: z.string(),
156
+ value: z.string(),
157
+ url: z.string().optional(),
158
+ tone: z.enum(["default", "success", "warning", "danger"]).optional(),
159
+ copyable: z.boolean().optional()
160
+ })
161
+ )
162
+ }).optional().describe("Persistent vessel reference record (CRM-style identity: name, contact, links) shown in the top bar. Replaces wholesale."),
152
163
  labels: z.array(z.string()).optional().describe("Triage labels (replace semantics)."),
153
164
  suggestions: z.array(z.string()).optional().describe("Quick-reply suggestion chips."),
154
165
  previewUrl: z.string().optional().describe("A single URL rendered as a link card."),
@@ -156,13 +167,13 @@ server.registerTool(
156
167
  vessel: z.string().optional().describe(`Vessel external_id (default "${defaultVessel}").`)
157
168
  }
158
169
  },
159
- async ({ message, body, title, card, pinCard, labels, suggestions, previewUrl, status, vessel }) => {
170
+ async ({ message, body, title, card, details, labels, suggestions, previewUrl, status, vessel }) => {
160
171
  const common = {
161
172
  vessel: vessel ?? defaultVessel,
162
173
  vesselTitle,
163
174
  ...title ? { title } : {},
164
175
  ...card ? { card } : {},
165
- ...pinCard ? { pinCard } : {},
176
+ ...details ? { details } : {},
166
177
  ...labels ? { labels } : {},
167
178
  ...suggestions ? { suggestions } : {},
168
179
  ...previewUrl ? { previewUrl } : {},
@@ -173,6 +184,35 @@ server.registerTool(
173
184
  return ok(`mid:${res.messageId}`);
174
185
  }
175
186
  );
187
+ server.registerTool(
188
+ "mark",
189
+ {
190
+ title: "Mark (action receipt)",
191
+ description: 'Record that you COMMITTED a discrete action, as a slim timeline chip \u2014 an icon + a short `label`, optional small `subtext`, optional deep-link `url`. Use it instead of narrating the action in prose (e.g. after a tool sends an email or records a payment): the chip is the durable record, so just keep working. Does not wait, fires no webhook. Returns the chip id as "mid:<id>".',
192
+ inputSchema: {
193
+ label: z.string().describe("The chip label \u2014 what happened (max 120 chars)."),
194
+ type: z.enum(["email_sent", "payment_recorded", "status_changed", "booking_created", "invoice_voided", "generic"]).optional().describe('Semantic kind \u2014 picks a default icon + tone. Defaults to "generic".'),
195
+ subtext: z.string().optional().describe("Small secondary line (an amount, an id, a target; max 160 chars)."),
196
+ icon: z.string().optional().describe('Override the default glyph with a literal emoji (e.g. "\u2709\uFE0F").'),
197
+ tone: z.enum(["neutral", "success", "warning", "danger"]).optional().describe('Accent colour. Defaults to "neutral".'),
198
+ url: z.string().optional().describe("Deep-link into your own UI \u2014 renders a right chevron. NOT an interaction."),
199
+ vessel: z.string().optional().describe(`Vessel external_id (default "${defaultVessel}").`)
200
+ }
201
+ },
202
+ async ({ label, type, subtext, icon, tone, url, vessel }) => {
203
+ const res = await v.mark({
204
+ vessel: vessel ?? defaultVessel,
205
+ vesselTitle,
206
+ label,
207
+ ...type ? { type } : {},
208
+ ...subtext ? { subtext } : {},
209
+ ...icon ? { icon } : {},
210
+ ...tone ? { tone } : {},
211
+ ...url ? { url } : {}
212
+ });
213
+ return ok(`mid:${res.markId}`);
214
+ }
215
+ );
176
216
  server.registerTool(
177
217
  "ask_human",
178
218
  {
@@ -327,21 +367,20 @@ server.registerTool(
327
367
  }
328
368
  },
329
369
  async ({ vessel, title, labels, status, archived, pinned }) => {
330
- const body = {
370
+ const patch = {
331
371
  ...title !== void 0 ? { title } : {},
332
372
  ...labels !== void 0 ? { labels } : {},
333
373
  ...status !== void 0 ? { vesselStatus: status } : {},
334
374
  ...archived !== void 0 ? { archived } : {},
335
375
  ...pinned !== void 0 ? { pinned } : {}
336
376
  };
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}`);
377
+ if (!Object.keys(patch).length) return ok("error: nothing to update");
378
+ try {
379
+ await v.updateVessel(vessel, patch);
380
+ return ok("ok");
381
+ } catch (e) {
382
+ return ok(`error: ${e.message}`);
383
+ }
345
384
  }
346
385
  );
347
386
  server.registerTool(
@@ -352,12 +391,37 @@ server.registerTool(
352
391
  inputSchema: { vessel: z.string().describe("Vessel external_id to delete.") }
353
392
  },
354
393
  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}`);
394
+ try {
395
+ await v.deleteVessel(vessel);
396
+ return ok(`deleted ${vessel}`);
397
+ } catch (e) {
398
+ return ok(`error: ${e.message}`);
399
+ }
400
+ }
401
+ );
402
+ server.registerTool(
403
+ "clear_messages",
404
+ {
405
+ title: "Clear a range of messages (rewind)",
406
+ 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.`,
407
+ inputSchema: {
408
+ vessel: z.string().optional().describe(`Vessel external_id (default "${defaultVessel}").`),
409
+ afterMessageId: z.string().optional().describe('Delete everything after this message id ("rewind to here").'),
410
+ beforeMessageId: z.string().optional().describe("Delete everything before this message id."),
411
+ after: z.string().optional().describe("Delete messages strictly newer than this ISO timestamp."),
412
+ before: z.string().optional().describe("Delete messages strictly older than this ISO timestamp."),
413
+ source: z.enum(["agent", "user", "all"]).optional().describe('Whose messages may go (default "agent" = agent+system).'),
414
+ dryRun: z.boolean().optional().describe("Count what would be deleted without deleting.")
415
+ }
416
+ },
417
+ async ({ vessel, afterMessageId, beforeMessageId, after, before, source, dryRun }) => {
418
+ const tgt = vessel ?? defaultVessel;
419
+ try {
420
+ const r = await v.clearMessages(tgt, { afterMessageId, beforeMessageId, after, before, source, dryRun });
421
+ return ok(r.dryRun ? `would delete ${r.deleted} message(s)` : `deleted ${r.deleted} message(s)`);
422
+ } catch (e) {
423
+ return ok(`error: ${e instanceof Error ? e.message : String(e)}`);
424
+ }
361
425
  }
362
426
  );
363
427
  await server.connect(new StdioServerTransport());
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vessels-mcp",
3
- "version": "0.3.0",
3
+ "version": "0.5.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.18.0"
23
+ "vessels-sdk": "^0.26.0"
24
24
  },
25
25
  "devDependencies": {
26
26
  "tsup": "^8.5.1",