@alacrity-ai/kbrelaymcp 0.1.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 ADDED
@@ -0,0 +1,74 @@
1
+ # @alacrity-ai/kbrelaymcp
2
+
3
+ An [MCP](https://modelcontextprotocol.io) server that gives an agent **kbRelay**
4
+ powers — projects, cards, the timeline, and your @-mentions — over kbRelay's HTTP
5
+ API. It's a thin, standalone stdio client: it carries no privileges of its own, so
6
+ the API token's tenant and **project access (RBAC)** govern exactly what it can see
7
+ and do.
8
+
9
+ ## Install (Claude Code / Claude Desktop)
10
+
11
+ ```bash
12
+ claude mcp add kbrelay --scope user \
13
+ --env KBRELAY_BASE_URL=https://kbrelay.lalalimited.com \
14
+ --env KBRELAY_API_KEY=<token from the web "API keys" panel> \
15
+ -- npx -y @alacrity-ai/kbrelaymcp
16
+ ```
17
+
18
+ `KBRELAY_BASE_URL` can point at the hosted deployment **or** a self-host instance
19
+ (e.g. `http://localhost:8080`), so the same MCP works against both.
20
+
21
+ ### Other MCP clients (Cursor / Windsurf / Cline)
22
+
23
+ ```json
24
+ {
25
+ "mcpServers": {
26
+ "kbrelay": {
27
+ "command": "npx",
28
+ "args": ["-y", "@alacrity-ai/kbrelaymcp"],
29
+ "env": {
30
+ "KBRELAY_BASE_URL": "https://kbrelay.lalalimited.com",
31
+ "KBRELAY_API_KEY": "<your token>"
32
+ }
33
+ }
34
+ }
35
+ }
36
+ ```
37
+
38
+ ## Configuration
39
+
40
+ | Env var | Required | Purpose |
41
+ |---|---|---|
42
+ | `KBRELAY_BASE_URL` | yes | kbRelay origin, e.g. `https://kbrelay.lalalimited.com` or `http://localhost:8080`. |
43
+ | `KBRELAY_API_KEY` | yes | A bearer token minted in the web **API keys** panel. Sent as `Authorization: Bearer …`. |
44
+
45
+ ## Tools
46
+
47
+ | Tool | Kind | What it does |
48
+ |---|---|---|
49
+ | `whoami` | read | The current user + tenant (call first for your user id). |
50
+ | `list_users` | read | Tenant users → resolve names/@handles to ids. |
51
+ | `list_projects` | read | Projects you can access (admins: all). |
52
+ | `get_project` | read | A project + its columns (resolve column ids by name). |
53
+ | `create_project` | write | New project (`code` required, seeds columns). |
54
+ | `list_cards` | read | Cards in a project (filter by column/assignee/q). |
55
+ | `get_card` | read | One card (read the spec before working it). |
56
+ | `create_card` | write | New card (markdown body; `@handle` to notify). |
57
+ | `update_card` | write | Edit and/or **move** (set `columnId` — status = column). |
58
+ | `delete_card` | write | Delete a card (cascades). |
59
+ | `get_timeline` | read | A card's activity log (events + comments). |
60
+ | `add_comment` | write | Report results on the timeline (note or handoff). |
61
+ | `redact_comment` | write | Soft-delete your own comment (leaves a tombstone). |
62
+ | `get_mentions` | read | Your @-mentions — "what did people ask me?". |
63
+ | `mark_mentions_read` | write | Acknowledge mentions after handling them. |
64
+
65
+ The `get_mentions` + `add_comment` pair makes *"check your mentions and respond to
66
+ them"* a first-class flow.
67
+
68
+ ## Requirements
69
+
70
+ Node ≥ 20. No configuration files — just the two env vars.
71
+
72
+ ## License
73
+
74
+ MIT
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env node
2
+ // kbRelay MCP — the binary `npx @alacrity-ai/kbrelaymcp` runs. Resolves the
3
+ // compiled stdio entrypoint relative to this shim and starts it. Any failure
4
+ // (missing config, transport error) prints to stderr — stdout is reserved for
5
+ // JSON-RPC — and exits 2.
6
+ import { fileURLToPath } from 'node:url';
7
+ import { dirname, join } from 'node:path';
8
+
9
+ const here = dirname(fileURLToPath(import.meta.url));
10
+
11
+ try {
12
+ const mod = await import(join(here, '..', 'dist', 'transport-stdio.js'));
13
+ await mod.startStdio();
14
+ } catch (err) {
15
+ process.stderr.write(`[kbrelaymcp] ${err instanceof Error ? err.message : String(err)}\n`);
16
+ process.exit(2);
17
+ }
@@ -0,0 +1,13 @@
1
+ import type { Config } from './config.js';
2
+ /**
3
+ * A tiny standalone typed fetch wrapper over kbRelay's HTTP API. Deliberately
4
+ * NOT a workspace dependency on @kbrelay/shared, so the published package is
5
+ * self-contained and `npx -y` needs no extra installs. Sends
6
+ * `Authorization: Bearer <KBRELAY_API_KEY>` (kbRelay's scheme); the token's
7
+ * tenant + RBAC project access govern exactly what it can see and do.
8
+ */
9
+ export declare class KbRelayClient {
10
+ private readonly config;
11
+ constructor(config: Config);
12
+ request<T = unknown>(method: string, path: string, body?: unknown): Promise<T>;
13
+ }
package/dist/client.js ADDED
@@ -0,0 +1,56 @@
1
+ /**
2
+ * A tiny standalone typed fetch wrapper over kbRelay's HTTP API. Deliberately
3
+ * NOT a workspace dependency on @kbrelay/shared, so the published package is
4
+ * self-contained and `npx -y` needs no extra installs. Sends
5
+ * `Authorization: Bearer <KBRELAY_API_KEY>` (kbRelay's scheme); the token's
6
+ * tenant + RBAC project access govern exactly what it can see and do.
7
+ */
8
+ export class KbRelayClient {
9
+ config;
10
+ constructor(config) {
11
+ this.config = config;
12
+ }
13
+ async request(method, path, body) {
14
+ const controller = new AbortController();
15
+ const timer = setTimeout(() => controller.abort(), 30_000);
16
+ try {
17
+ const res = await fetch(`${this.config.baseUrl}/api${path}`, {
18
+ method,
19
+ headers: {
20
+ 'content-type': 'application/json',
21
+ authorization: `Bearer ${this.config.apiKey}`,
22
+ },
23
+ body: body === undefined ? undefined : JSON.stringify(body),
24
+ signal: controller.signal,
25
+ });
26
+ const text = await res.text();
27
+ let json = null;
28
+ if (text) {
29
+ try {
30
+ json = JSON.parse(text);
31
+ }
32
+ catch {
33
+ /* non-JSON body */
34
+ }
35
+ }
36
+ if (!res.ok) {
37
+ const err = json;
38
+ let message = err?.error ?? `kbRelay request failed (${res.status})`;
39
+ if (err?.details) {
40
+ message += ` — ${Object.entries(err.details).map(([k, v]) => `${k}: ${v}`).join('; ')}`;
41
+ }
42
+ throw new Error(message);
43
+ }
44
+ return json;
45
+ }
46
+ catch (err) {
47
+ if (err instanceof Error && err.name === 'AbortError') {
48
+ throw new Error('kbRelay request timed out after 30s', { cause: err });
49
+ }
50
+ throw err;
51
+ }
52
+ finally {
53
+ clearTimeout(timer);
54
+ }
55
+ }
56
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * MCP config — two env vars, passed via `claude mcp add … --env …`. No TOML /
3
+ * multi-profile machinery (KISS). Missing either var throws a clear error that
4
+ * the bin shim prints to stderr before exiting.
5
+ */
6
+ export interface Config {
7
+ baseUrl: string;
8
+ apiKey: string;
9
+ }
10
+ export declare class ConfigError extends Error {
11
+ }
12
+ export declare function loadConfig(env?: NodeJS.ProcessEnv): Config;
package/dist/config.js ADDED
@@ -0,0 +1,13 @@
1
+ export class ConfigError extends Error {
2
+ }
3
+ export function loadConfig(env = process.env) {
4
+ const baseUrl = env.KBRELAY_BASE_URL?.replace(/\/+$/, '');
5
+ const apiKey = env.KBRELAY_API_KEY;
6
+ if (!baseUrl) {
7
+ throw new ConfigError('KBRELAY_BASE_URL is not set — e.g. https://kbrelay.lalalimited.com (or http://localhost:8080 for self-host).');
8
+ }
9
+ if (!apiKey) {
10
+ throw new ConfigError('KBRELAY_API_KEY is not set — mint a token in the kbRelay web "API keys" panel.');
11
+ }
12
+ return { baseUrl, apiKey };
13
+ }
@@ -0,0 +1,18 @@
1
+ import type { ZodTypeAny, infer as ZodInfer } from 'zod';
2
+ import type { KbRelayClient } from './client.js';
3
+ /** A tool as declared: a zod input schema + a handler that calls the client. */
4
+ export interface ToolDef<S extends ZodTypeAny> {
5
+ name: string;
6
+ /** ≤200 chars; teaches the model kbRelay's conventions. */
7
+ description: string;
8
+ inputSchema: S;
9
+ handler: (args: ZodInfer<S>, client: KbRelayClient) => Promise<unknown>;
10
+ }
11
+ /** A registered tool: JSON Schema for ListTools + a validated runner for CallTool. */
12
+ export interface Tool {
13
+ name: string;
14
+ description: string;
15
+ inputSchema: Record<string, unknown>;
16
+ run: (args: unknown, client: KbRelayClient) => Promise<unknown>;
17
+ }
18
+ export declare function defineTool<S extends ZodTypeAny>(def: ToolDef<S>): Tool;
@@ -0,0 +1,15 @@
1
+ import { zodToJsonSchema } from 'zod-to-json-schema';
2
+ export function defineTool(def) {
3
+ const jsonSchema = zodToJsonSchema(def.inputSchema, {
4
+ target: 'jsonSchema7',
5
+ $refStrategy: 'none',
6
+ });
7
+ delete jsonSchema.$schema;
8
+ return {
9
+ name: def.name,
10
+ description: def.description,
11
+ inputSchema: jsonSchema,
12
+ // async so a zod validation error surfaces as a rejected promise.
13
+ run: async (args, client) => def.handler(def.inputSchema.parse(args ?? {}), client),
14
+ };
15
+ }
@@ -0,0 +1,11 @@
1
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
2
+ import type { KbRelayClient } from './client.js';
3
+ /**
4
+ * The low-level MCP server: advertises `allTools` for ListTools, and for
5
+ * CallTool finds the tool by name, validates+runs it (zod validation happens
6
+ * inside the tool's runner), and returns the JSON result as text content
7
+ * (`isError: true` on failure).
8
+ */
9
+ export declare function createServer(opts: {
10
+ client: KbRelayClient;
11
+ }): Server;
package/dist/server.js ADDED
@@ -0,0 +1,36 @@
1
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
2
+ import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
3
+ import { allTools } from './tools/index.js';
4
+ /**
5
+ * The low-level MCP server: advertises `allTools` for ListTools, and for
6
+ * CallTool finds the tool by name, validates+runs it (zod validation happens
7
+ * inside the tool's runner), and returns the JSON result as text content
8
+ * (`isError: true` on failure).
9
+ */
10
+ export function createServer(opts) {
11
+ const server = new Server({ name: 'kbrelaymcp', version: '0.1.0' }, { capabilities: { tools: {} } });
12
+ server.setRequestHandler(ListToolsRequestSchema, () => ({
13
+ tools: allTools.map((t) => ({
14
+ name: t.name,
15
+ description: t.description,
16
+ inputSchema: t.inputSchema,
17
+ })),
18
+ }));
19
+ server.setRequestHandler(CallToolRequestSchema, async (req) => {
20
+ const tool = allTools.find((t) => t.name === req.params.name);
21
+ if (!tool) {
22
+ return { content: [{ type: 'text', text: `Unknown tool: ${req.params.name}` }], isError: true };
23
+ }
24
+ try {
25
+ const result = await tool.run(req.params.arguments, opts.client);
26
+ return { content: [{ type: 'text', text: JSON.stringify(result ?? { ok: true }, null, 2) }] };
27
+ }
28
+ catch (err) {
29
+ return {
30
+ content: [{ type: 'text', text: err instanceof Error ? err.message : String(err) }],
31
+ isError: true,
32
+ };
33
+ }
34
+ });
35
+ return server;
36
+ }
@@ -0,0 +1,2 @@
1
+ import { type Tool } from '../define-tool.js';
2
+ export declare const allTools: Tool[];
@@ -0,0 +1,161 @@
1
+ import { z } from 'zod';
2
+ import { defineTool } from '../define-tool.js';
3
+ /**
4
+ * The kbRelay MCP tool surface. Every tool is RBAC-scoped by the token — a tool
5
+ * only sees/touches projects the token's user can access. Descriptions teach the
6
+ * model kbRelay's conventions: status = column (move, don't set a field), report
7
+ * on the timeline (don't rewrite the description), write markdown, `@handle` to
8
+ * notify, refer to tickets by key (OBL-1).
9
+ */
10
+ const qs = (params) => {
11
+ const pairs = Object.entries(params).filter(([, v]) => v != null && v !== '');
12
+ return pairs.length ? '?' + pairs.map(([k, v]) => `${k}=${encodeURIComponent(v)}`).join('&') : '';
13
+ };
14
+ const enc = encodeURIComponent;
15
+ export const allTools = [
16
+ // ── Identity ──
17
+ defineTool({
18
+ name: 'whoami',
19
+ description: 'Who this token is: the current user (id, name, kind, role) and tenant. Call first to learn your own user id.',
20
+ inputSchema: z.object({}),
21
+ handler: (_a, c) => c.request('GET', '/v1/me'),
22
+ }),
23
+ defineTool({
24
+ 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.',
26
+ inputSchema: z.object({}),
27
+ handler: (_a, c) => c.request('GET', '/v1/users'),
28
+ }),
29
+ // ── Projects ──
30
+ defineTool({
31
+ name: 'list_projects',
32
+ description: 'List projects you can access (admins: all). Each has a code (e.g. OBL) that prefixes its ticket keys.',
33
+ inputSchema: z.object({ status: z.enum(['active', 'archived']).optional() }),
34
+ handler: (a, c) => c.request('GET', `/v1/projects${qs({ status: a.status })}`),
35
+ }),
36
+ defineTool({
37
+ name: 'get_project',
38
+ description: 'Get one project and its columns. Column ids are per-project — always resolve them by name here, never hardcode.',
39
+ inputSchema: z.object({ projectId: z.string() }),
40
+ handler: (a, c) => c.request('GET', `/v1/projects/${enc(a.projectId)}`),
41
+ }),
42
+ defineTool({
43
+ name: 'create_project',
44
+ description: 'Create a project. `code` (2–6 alnum, e.g. LSEO) is required and prefixes ticket keys; default columns are seeded.',
45
+ inputSchema: z.object({
46
+ name: z.string(),
47
+ code: z.string(),
48
+ description: z.string().nullish(),
49
+ color: z.string().nullish(),
50
+ }),
51
+ handler: (a, c) => c.request('POST', '/v1/projects', a),
52
+ }),
53
+ // ── Cards ──
54
+ defineTool({
55
+ name: 'list_cards',
56
+ description: 'List cards in a project. Optional filters: column (id), assignee (user id), q (text search on summary/description).',
57
+ inputSchema: z.object({
58
+ projectId: z.string(),
59
+ column: z.string().optional(),
60
+ assignee: z.string().optional(),
61
+ q: z.string().optional(),
62
+ }),
63
+ handler: (a, c) => c.request('GET', `/v1/projects/${enc(a.projectId)}/cards${qs({ column: a.column, assignee: a.assignee, q: a.q })}`),
64
+ }),
65
+ defineTool({
66
+ name: 'get_card',
67
+ description: 'Get one card by id (summary, description, acceptanceCriteria, column, assignee, ticket key). Read the spec before working it.',
68
+ inputSchema: z.object({ cardId: z.string() }),
69
+ handler: (a, c) => c.request('GET', `/v1/cards/${enc(a.cardId)}`),
70
+ }),
71
+ defineTool({
72
+ name: 'create_card',
73
+ description: 'Create a card (defaults to the first column). Write summary plain; description/acceptanceCriteria in markdown; @handle to notify.',
74
+ inputSchema: z.object({
75
+ projectId: z.string(),
76
+ summary: z.string(),
77
+ description: z.string().nullish(),
78
+ acceptanceCriteria: z.string().nullish(),
79
+ columnId: z.string().optional(),
80
+ assigneeUserId: z.string().nullish(),
81
+ }),
82
+ handler: (a, c) => {
83
+ const { projectId, ...body } = a;
84
+ return c.request('POST', `/v1/projects/${enc(projectId)}/cards`, body);
85
+ },
86
+ }),
87
+ defineTool({
88
+ name: 'update_card',
89
+ 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.',
90
+ inputSchema: z.object({
91
+ cardId: z.string(),
92
+ summary: z.string().optional(),
93
+ description: z.string().nullish(),
94
+ acceptanceCriteria: z.string().nullish(),
95
+ columnId: z.string().optional(),
96
+ assigneeUserId: z.string().nullish(),
97
+ position: z.number().optional(),
98
+ }),
99
+ handler: (a, c) => {
100
+ const { cardId, ...body } = a;
101
+ return c.request('PATCH', `/v1/cards/${enc(cardId)}`, body);
102
+ },
103
+ }),
104
+ defineTool({
105
+ name: 'delete_card',
106
+ description: 'Delete a card (cascades its timeline + mentions). Irreversible.',
107
+ inputSchema: z.object({ cardId: z.string() }),
108
+ handler: (a, c) => c.request('DELETE', `/v1/cards/${enc(a.cardId)}`),
109
+ }),
110
+ // ── Timeline ──
111
+ defineTool({
112
+ name: 'get_timeline',
113
+ description: 'The card\'s activity log (system events + comments), oldest→newest — the who-did-what-when history.',
114
+ inputSchema: z.object({ cardId: z.string() }),
115
+ handler: (a, c) => c.request('GET', `/v1/cards/${enc(a.cardId)}/timeline`),
116
+ }),
117
+ defineTool({
118
+ name: 'add_comment',
119
+ description: 'Report results ON the timeline (not by editing the description). A note or a structured handoff. Body is markdown; @handle to notify.',
120
+ inputSchema: z.object({
121
+ cardId: z.string(),
122
+ type: z.enum(['note', 'handoff']).default('note'),
123
+ body: z.string(),
124
+ meta: z
125
+ .object({
126
+ summary: z.string().optional(),
127
+ evidence: z.array(z.string()).optional(),
128
+ verify: z.array(z.string()).optional(),
129
+ spunOff: z.array(z.string()).optional(),
130
+ })
131
+ .optional(),
132
+ }),
133
+ handler: (a, c) => {
134
+ const { cardId, ...body } = a;
135
+ return c.request('POST', `/v1/cards/${enc(cardId)}/comments`, body);
136
+ },
137
+ }),
138
+ defineTool({
139
+ name: 'redact_comment',
140
+ description: 'Redact (soft-delete) YOUR OWN comment — leaves a tombstone. For a leaked secret / PII / wrong-card post. Author-only.',
141
+ inputSchema: z.object({ cardId: z.string(), commentId: z.string() }),
142
+ handler: (a, c) => c.request('DELETE', `/v1/cards/${enc(a.cardId)}/comments/${enc(a.commentId)}`),
143
+ }),
144
+ // ── Mentions (your inbox) ──
145
+ defineTool({
146
+ name: 'get_mentions',
147
+ description: 'Your @-mentions (default unread). Side-effect-free — listing does NOT clear them. This is how you find "what did people ask me?".',
148
+ inputSchema: z.object({ status: z.enum(['unread', 'read', 'all']).optional() }),
149
+ handler: (a, c) => c.request('GET', `/v1/me/mentions${qs({ status: a.status })}`),
150
+ }),
151
+ defineTool({
152
+ name: 'mark_mentions_read',
153
+ description: 'Acknowledge mentions after handling them: pass mentionIds, or all:true to clear everything.',
154
+ inputSchema: z
155
+ .object({ mentionIds: z.array(z.string()).optional(), all: z.boolean().optional() })
156
+ .refine((v) => v.all || (v.mentionIds && v.mentionIds.length > 0), {
157
+ message: 'Provide mentionIds or all:true',
158
+ }),
159
+ handler: (a, c) => c.request('POST', '/v1/me/mentions/read', a),
160
+ }),
161
+ ];
@@ -0,0 +1,7 @@
1
+ /**
2
+ * stdio entrypoint. Resolves config, logs the active base URL to **stderr**
3
+ * (stdout is reserved for the JSON-RPC protocol), and connects the server over
4
+ * stdio. Thrown config errors propagate to the bin shim, which prints them and
5
+ * exits 2.
6
+ */
7
+ export declare function startStdio(): Promise<void>;
@@ -0,0 +1,17 @@
1
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
2
+ import { loadConfig } from './config.js';
3
+ import { KbRelayClient } from './client.js';
4
+ import { createServer } from './server.js';
5
+ /**
6
+ * stdio entrypoint. Resolves config, logs the active base URL to **stderr**
7
+ * (stdout is reserved for the JSON-RPC protocol), and connects the server over
8
+ * stdio. Thrown config errors propagate to the bin shim, which prints them and
9
+ * exits 2.
10
+ */
11
+ export async function startStdio() {
12
+ const config = loadConfig();
13
+ process.stderr.write(`[kbrelaymcp] kbRelay MCP server → ${config.baseUrl}\n`);
14
+ const client = new KbRelayClient(config);
15
+ const server = createServer({ client });
16
+ await server.connect(new StdioServerTransport());
17
+ }
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "@alacrity-ai/kbrelaymcp",
3
+ "version": "0.1.0",
4
+ "description": "MCP server for kbRelay — give an agent kanban powers (projects, cards, timeline, mentions) over the kbRelay API.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "private": false,
8
+ "bin": {
9
+ "kbrelaymcp": "./bin/kbrelaymcp.mjs"
10
+ },
11
+ "files": [
12
+ "dist",
13
+ "bin",
14
+ "README.md"
15
+ ],
16
+ "publishConfig": {
17
+ "access": "public"
18
+ },
19
+ "engines": {
20
+ "node": ">=20"
21
+ },
22
+ "scripts": {
23
+ "build": "tsc -p tsconfig.build.json",
24
+ "prepublishOnly": "npm run build",
25
+ "typecheck": "tsc --noEmit -p tsconfig.build.json",
26
+ "test": "vitest run"
27
+ },
28
+ "keywords": [
29
+ "mcp",
30
+ "model-context-protocol",
31
+ "kbrelay",
32
+ "kanban",
33
+ "agents"
34
+ ],
35
+ "dependencies": {
36
+ "@modelcontextprotocol/sdk": "^1.12.0",
37
+ "zod": "^3.24.1",
38
+ "zod-to-json-schema": "^3.24.1"
39
+ },
40
+ "devDependencies": {
41
+ "@types/node": "^24.0.0",
42
+ "typescript": "~5.9.3",
43
+ "vitest": "^2.1.0"
44
+ }
45
+ }