@alacrity-ai/kbrelaymcp 0.3.1 → 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.
package/README.md CHANGED
@@ -54,18 +54,23 @@ claude mcp add kbrelay --scope user \
54
54
  | `create_project` | write | New project (`code` required, seeds columns). |
55
55
  | `update_project` | write | Edit a project's name/code/description/color/status. |
56
56
  | `list_cards` | read | Cards in a project (filter by column/assignee/q). |
57
- | `get_card` | read | One card (read the spec before working it). |
57
+ | `get_card` | read | One card (read the spec before working it). Returns `attachments[]` — each with `filename`, `kind`, `sizeBytes`, and a same-origin `url` to fetch the bytes (v0.16.0). |
58
58
  | `create_card` | write | New card (markdown body; `@handle` to notify). |
59
59
  | `update_card` | write | Edit and/or **move** (set `columnId` — status = column). |
60
60
  | `delete_card` | write | Delete a card (cascades). |
61
61
  | `get_timeline` | read | A card's activity log (events + comments). |
62
- | `add_comment` | write | Report results on the timeline (note or handoff). |
62
+ | `get_project_activity` | read | Newest-first card events across a whole board — "what happened while I was away?" (cursor-paginated, v0.17.0). |
63
+ | `add_comment` | write | Report results on the timeline (note or handoff); `attachmentIds` links uploaded files. |
64
+ | `add_attachment` | write | Upload a file to a card (`filePath` or base64, ≤25 MB) → attachment + markdown snippet (v0.17.0). |
65
+ | `delete_attachment` | write | Delete an attachment (uploader or admin; bytes purged). |
63
66
  | `redact_comment` | write | Soft-delete your own comment (leaves a tombstone). |
67
+ | `list_my_queue` | read | Your actionable queue — cards assigned to you in a `ready`-role column (v0.15.0). Work these first. |
64
68
  | `get_mentions` | read | Your @-mentions — "what did people ask me?". |
65
69
  | `mark_mentions_read` | write | Acknowledge mentions after handling them. |
66
70
 
67
71
  The `get_mentions` + `add_comment` pair makes *"check your mentions and respond to
68
- them"* a first-class flow.
72
+ them"* a first-class flow, and `add_attachment` + `add_comment` (`attachmentIds`)
73
+ makes *"attach the evidence to the handoff"* one too.
69
74
 
70
75
  ## Requirements
71
76
 
package/dist/client.d.ts CHANGED
@@ -10,4 +10,15 @@ export declare class KbRelayClient {
10
10
  private readonly config;
11
11
  constructor(config: Config);
12
12
  request<T = unknown>(method: string, path: string, body?: unknown): Promise<T>;
13
+ /**
14
+ * Multipart upload (v0.17.0, KBR-66) — POST one file part named `file`, the
15
+ * shape `POST /cards/:id/attachments` expects. fetch sets the boundary; we
16
+ * must NOT set content-type ourselves.
17
+ */
18
+ upload<T = unknown>(path: string, file: {
19
+ data: Uint8Array;
20
+ filename: string;
21
+ contentType?: string;
22
+ }): Promise<T>;
23
+ private send;
13
24
  }
package/dist/client.js CHANGED
@@ -11,16 +11,32 @@ export class KbRelayClient {
11
11
  this.config = config;
12
12
  }
13
13
  async request(method, path, body) {
14
+ return this.send(method, path, {
15
+ headers: { 'content-type': 'application/json' },
16
+ body: body === undefined ? undefined : JSON.stringify(body),
17
+ });
18
+ }
19
+ /**
20
+ * Multipart upload (v0.17.0, KBR-66) — POST one file part named `file`, the
21
+ * shape `POST /cards/:id/attachments` expects. fetch sets the boundary; we
22
+ * must NOT set content-type ourselves.
23
+ */
24
+ async upload(path, file) {
25
+ const form = new FormData();
26
+ form.set('file', new Blob([file.data], { type: file.contentType ?? 'application/octet-stream' }), file.filename);
27
+ return this.send('POST', path, { body: form });
28
+ }
29
+ async send(method, path, init) {
14
30
  const controller = new AbortController();
15
31
  const timer = setTimeout(() => controller.abort(), 30_000);
16
32
  try {
17
33
  const res = await fetch(`${this.config.baseUrl}/api${path}`, {
18
34
  method,
19
35
  headers: {
20
- 'content-type': 'application/json',
36
+ ...init.headers,
21
37
  authorization: `Bearer ${this.config.apiKey}`,
22
38
  },
23
- body: body === undefined ? undefined : JSON.stringify(body),
39
+ body: init.body,
24
40
  signal: controller.signal,
25
41
  });
26
42
  const text = await res.text();
@@ -1,4 +1,6 @@
1
1
  import { z } from 'zod';
2
+ import { readFile } from 'node:fs/promises';
3
+ import { basename, extname } from 'node:path';
2
4
  import { defineTool } from '../define-tool.js';
3
5
  /**
4
6
  * The kbRelay MCP tool surface. Every tool is RBAC-scoped by the token — a tool
@@ -12,6 +14,17 @@ const qs = (params) => {
12
14
  return pairs.length ? '?' + pairs.map(([k, v]) => `${k}=${encodeURIComponent(v)}`).join('&') : '';
13
15
  };
14
16
  const enc = encodeURIComponent;
17
+ /** Server-side cap on POST /cards/:id/attachments — checked here too for a clear error. */
18
+ const MAX_ATTACHMENT_BYTES = 25 * 1024 * 1024;
19
+ /** Extension → content type for filePath uploads, so the server classifies the
20
+ * kind correctly (image ⇒ inline render). Unknown ⇒ octet-stream. */
21
+ const MIME_BY_EXT = {
22
+ '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.gif': 'image/gif',
23
+ '.webp': 'image/webp', '.svg': 'image/svg+xml', '.pdf': 'application/pdf',
24
+ '.txt': 'text/plain', '.md': 'text/markdown', '.csv': 'text/csv',
25
+ '.json': 'application/json', '.html': 'text/html',
26
+ '.zip': 'application/zip', '.gz': 'application/gzip', '.tar': 'application/x-tar',
27
+ };
15
28
  export const allTools = [
16
29
  // ── Identity ──
17
30
  defineTool({
@@ -22,7 +35,7 @@ export const allTools = [
22
35
  }),
23
36
  defineTool({
24
37
  name: 'list_users',
25
- description: 'List the tenant\'s users (id, name, kind human/agent, @handle). Use to resolve a name to an id for assignment, or a @handle to mention.',
38
+ description: 'List the tenant\'s users (id, name, kind, @handle, profile). Resolve a nameid for assignment or a @handle to mention; read `profile` for who a person is (role/persona) and how to weigh their feedback.',
26
39
  inputSchema: z.object({}),
27
40
  handler: (_a, c) => c.request('GET', '/v1/users'),
28
41
  }),
@@ -35,7 +48,7 @@ export const allTools = [
35
48
  }),
36
49
  defineTool({
37
50
  name: 'get_project',
38
- description: 'Get one project (name, code, description, color, status) and its columns. Read `description` to learn what the project is for. Column ids are per-project resolve by name here, never hardcode.',
51
+ description: 'Get a project (name, code, description, color, status) + its columns, each with an optional `role`. Read `description` for context. Resolve target columns by `role`, never by hardcoded name.',
39
52
  inputSchema: z.object({ projectId: z.string() }),
40
53
  handler: (a, c) => c.request('GET', `/v1/projects/${enc(a.projectId)}`),
41
54
  }),
@@ -69,14 +82,15 @@ export const allTools = [
69
82
  // ── Cards ──
70
83
  defineTool({
71
84
  name: 'list_cards',
72
- description: 'List cards in a project. Optional filters: column (id), assignee (user id), q (text search on summary/description).',
85
+ description: 'List cards in a project. Optional filters: column (id), assignee (user id), q (text search on summary/description), archived=true (archived cards only; default excludes them).',
73
86
  inputSchema: z.object({
74
87
  projectId: z.string(),
75
88
  column: z.string().optional(),
76
89
  assignee: z.string().optional(),
77
90
  q: z.string().optional(),
91
+ archived: z.boolean().optional(),
78
92
  }),
79
- handler: (a, c) => c.request('GET', `/v1/projects/${enc(a.projectId)}/cards${qs({ column: a.column, assignee: a.assignee, q: a.q })}`),
93
+ handler: (a, c) => c.request('GET', `/v1/projects/${enc(a.projectId)}/cards${qs({ column: a.column, assignee: a.assignee, q: a.q, archived: a.archived ? '1' : undefined })}`),
80
94
  }),
81
95
  defineTool({
82
96
  name: 'get_card',
@@ -86,7 +100,7 @@ export const allTools = [
86
100
  }),
87
101
  defineTool({
88
102
  name: 'create_card',
89
- description: 'Create a card (defaults to the first column). Write summary plain; description/acceptanceCriteria in markdown; @handle to notify.',
103
+ description: 'Create a card (defaults to the first column). Write summary plain; description/acceptanceCriteria in markdown; @handle to notify. dueAt = epoch-ms deadline; labels = names (400 on unknown).',
90
104
  inputSchema: z.object({
91
105
  projectId: z.string(),
92
106
  summary: z.string(),
@@ -94,15 +108,21 @@ export const allTools = [
94
108
  acceptanceCriteria: z.string().nullish(),
95
109
  columnId: z.string().optional(),
96
110
  assigneeUserId: z.string().nullish(),
111
+ reviewerUserId: z.string().nullish(),
112
+ dueAt: z.number().nullish(),
113
+ labels: z.array(z.string()).optional(),
97
114
  }),
98
115
  handler: (a, c) => {
99
- const { projectId, ...body } = a;
100
- return c.request('POST', `/v1/projects/${enc(projectId)}/cards`, body);
116
+ const { projectId, labels, ...body } = a;
117
+ return c.request('POST', `/v1/projects/${enc(projectId)}/cards`, {
118
+ ...body,
119
+ ...(labels !== undefined ? { labelNames: labels } : {}),
120
+ });
101
121
  },
102
122
  }),
103
123
  defineTool({
104
124
  name: 'update_card',
105
- description: 'Edit a card AND/OR move it. To change status, set columnId (status = column). Rewrite the spec in place; do NOT log progress here — use add_comment.',
125
+ description: 'Edit and/or move a card (status = column). Pickup in_progress; finish → review + set reviewerUserId (default: card creator); done only when told; stuck blocked. Log via add_comment.',
106
126
  inputSchema: z.object({
107
127
  cardId: z.string(),
108
128
  summary: z.string().optional(),
@@ -110,11 +130,18 @@ export const allTools = [
110
130
  acceptanceCriteria: z.string().nullish(),
111
131
  columnId: z.string().optional(),
112
132
  assigneeUserId: z.string().nullish(),
133
+ reviewerUserId: z.string().nullish(),
134
+ dueAt: z.number().nullish(),
135
+ archived: z.boolean().optional(),
136
+ labels: z.array(z.string()).optional(),
113
137
  position: z.number().optional(),
114
138
  }),
115
139
  handler: (a, c) => {
116
- const { cardId, ...body } = a;
117
- return c.request('PATCH', `/v1/cards/${enc(cardId)}`, body);
140
+ const { cardId, labels, ...body } = a;
141
+ return c.request('PATCH', `/v1/cards/${enc(cardId)}`, {
142
+ ...body,
143
+ ...(labels !== undefined ? { labelNames: labels } : {}),
144
+ });
118
145
  },
119
146
  }),
120
147
  defineTool({
@@ -123,6 +150,21 @@ export const allTools = [
123
150
  inputSchema: z.object({ cardId: z.string() }),
124
151
  handler: (a, c) => c.request('DELETE', `/v1/cards/${enc(a.cardId)}`),
125
152
  }),
153
+ defineTool({
154
+ name: 'get_project_activity',
155
+ description: 'Project activity feed: newest-first card events across the board (creates/moves/assigns/comments) with cardKey + cardSummary. Catch up on "what happened while I was away"; nextCursor pages older.',
156
+ inputSchema: z.object({
157
+ projectId: z.string(),
158
+ since: z.number().optional(),
159
+ limit: z.number().optional(),
160
+ cursor: z.string().optional(),
161
+ }),
162
+ handler: (a, c) => c.request('GET', `/v1/projects/${enc(a.projectId)}/events${qs({
163
+ since: a.since != null ? String(a.since) : undefined,
164
+ limit: a.limit != null ? String(a.limit) : undefined,
165
+ cursor: a.cursor,
166
+ })}`),
167
+ }),
126
168
  // ── Timeline ──
127
169
  defineTool({
128
170
  name: 'get_timeline',
@@ -132,7 +174,7 @@ export const allTools = [
132
174
  }),
133
175
  defineTool({
134
176
  name: 'add_comment',
135
- description: 'Report results ON the timeline (not by editing the description). A note or a structured handoff. Body is markdown; @handle to notify.',
177
+ description: 'Report results ON the timeline (not by editing the description). A note or a structured handoff. Body is markdown; @handle to notify; attachmentIds links files uploaded via add_attachment.',
136
178
  inputSchema: z.object({
137
179
  cardId: z.string(),
138
180
  type: z.enum(['note', 'handoff']).default('note'),
@@ -145,18 +187,74 @@ export const allTools = [
145
187
  spunOff: z.array(z.string()).optional(),
146
188
  })
147
189
  .optional(),
190
+ attachmentIds: z.array(z.string()).optional(),
148
191
  }),
149
192
  handler: (a, c) => {
150
193
  const { cardId, ...body } = a;
151
194
  return c.request('POST', `/v1/cards/${enc(cardId)}/comments`, body);
152
195
  },
153
196
  }),
197
+ // ── Attachments (v0.17.0, KBR-66) ──
198
+ defineTool({
199
+ name: 'add_attachment',
200
+ description: 'Attach a file to a card: filePath (preferred) OR contentBase64+filename. ≤25 MB. Returns the attachment + a ready-to-paste markdown snippet; link it to a note/handoff via add_comment attachmentIds.',
201
+ inputSchema: z
202
+ .object({
203
+ cardId: z.string(),
204
+ filePath: z.string().optional(),
205
+ contentBase64: z.string().optional(),
206
+ filename: z.string().optional(),
207
+ contentType: z.string().optional(),
208
+ })
209
+ .refine((v) => (v.filePath != null) !== (v.contentBase64 != null), {
210
+ message: 'Provide exactly one of filePath or contentBase64',
211
+ })
212
+ .refine((v) => v.contentBase64 == null || !!v.filename, {
213
+ message: 'filename is required with contentBase64',
214
+ }),
215
+ handler: async (a, c) => {
216
+ let data;
217
+ let filename;
218
+ if (a.filePath != null) {
219
+ data = new Uint8Array(await readFile(a.filePath));
220
+ filename = a.filename ?? basename(a.filePath);
221
+ }
222
+ else {
223
+ data = new Uint8Array(Buffer.from(a.contentBase64, 'base64'));
224
+ filename = a.filename;
225
+ }
226
+ if (data.byteLength > MAX_ATTACHMENT_BYTES) {
227
+ throw new Error(`File too large: ${(data.byteLength / (1024 * 1024)).toFixed(1)} MB (max 25 MB)`);
228
+ }
229
+ const contentType = a.contentType ?? MIME_BY_EXT[extname(filename).toLowerCase()];
230
+ const res = await c.upload(`/v1/cards/${enc(a.cardId)}/attachments`, { data, filename, contentType });
231
+ const att = res.attachment;
232
+ // Same snippet convention as the web composer: images inline, else a link.
233
+ const markdown = att.kind === 'image'
234
+ ? `![${att.filename}](${att.url})`
235
+ : `[📎 ${att.filename}](${att.url})`;
236
+ return { ...res, markdown };
237
+ },
238
+ }),
239
+ defineTool({
240
+ name: 'delete_attachment',
241
+ description: 'Delete an attachment (its bytes are purged). Uploader or admin only — others get 403. Remember to edit out any markdown referencing it.',
242
+ inputSchema: z.object({ attachmentId: z.string() }),
243
+ handler: (a, c) => c.request('DELETE', `/v1/attachments/${enc(a.attachmentId)}`),
244
+ }),
154
245
  defineTool({
155
246
  name: 'redact_comment',
156
247
  description: 'Redact (soft-delete) YOUR OWN comment — leaves a tombstone. For a leaked secret / PII / wrong-card post. Author-only.',
157
248
  inputSchema: z.object({ cardId: z.string(), commentId: z.string() }),
158
249
  handler: (a, c) => c.request('DELETE', `/v1/cards/${enc(a.cardId)}/comments/${enc(a.commentId)}`),
159
250
  }),
251
+ // ── Your queue (what to work now) ──
252
+ defineTool({
253
+ name: 'list_my_queue',
254
+ description: 'Your queue, two sections: work = assigned to you in a ready column (do these); review = you are the reviewer in a review column (verify these). Due-soonest first; finish → review + handoff.',
255
+ inputSchema: z.object({ projectId: z.string().optional() }),
256
+ handler: (a, c) => c.request('GET', `/v1/me/queue${qs({ projectId: a.projectId })}`),
257
+ }),
160
258
  // ── Mentions (your inbox) ──
161
259
  defineTool({
162
260
  name: 'get_mentions',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alacrity-ai/kbrelaymcp",
3
- "version": "0.3.1",
3
+ "version": "0.5.0",
4
4
  "description": "MCP server for kbRelay — give an agent kanban powers (projects, cards, timeline, mentions) over the kbRelay API.",
5
5
  "type": "module",
6
6
  "license": "MIT",