@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 +8 -3
- package/dist/client.d.ts +11 -0
- package/dist/client.js +18 -2
- package/dist/tools/index.js +109 -11
- package/package.json +1 -1
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
|
-
| `
|
|
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
|
-
|
|
36
|
+
...init.headers,
|
|
21
37
|
authorization: `Bearer ${this.config.apiKey}`,
|
|
22
38
|
},
|
|
23
|
-
body:
|
|
39
|
+
body: init.body,
|
|
24
40
|
signal: controller.signal,
|
|
25
41
|
});
|
|
26
42
|
const text = await res.text();
|
package/dist/tools/index.js
CHANGED
|
@@ -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
|
|
38
|
+
description: 'List the tenant\'s users (id, name, kind, @handle, profile). Resolve a name→id 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
|
|
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`,
|
|
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
|
|
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)}`,
|
|
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
|
+
? ``
|
|
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