@roadmapperai/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.
- package/README.md +9 -3
- package/package.json +1 -1
- package/server.mjs +213 -39
package/README.md
CHANGED
|
@@ -31,15 +31,21 @@ Add to your `~/.config/claude-code/config.json` (or the equivalent
|
|
|
31
31
|
"command": "npx",
|
|
32
32
|
"args": ["-y", "@roadmapperai/mcp"],
|
|
33
33
|
"env": {
|
|
34
|
-
"
|
|
35
|
-
"
|
|
36
|
-
"
|
|
34
|
+
"ROADMAPPER_BACKEND_URL": "https://api.roadmapperai.com",
|
|
35
|
+
"ROADMAPPER_PUBLISHABLE_KEY": "sb_publishable_...",
|
|
36
|
+
"ROADMAPPER_WORKSPACE_ID": "<your workspace id>",
|
|
37
|
+
"ROADMAPPER_API_KEY": "<rmpr_... — optional, enables writes>"
|
|
37
38
|
}
|
|
38
39
|
}
|
|
39
40
|
}
|
|
40
41
|
}
|
|
41
42
|
```
|
|
42
43
|
|
|
44
|
+
The `ROADMAPPER_API_KEY` env var is optional. Without it, the install runs
|
|
45
|
+
read-only (list/get/suggest tools). Mint a key in the dashboard at Settings →
|
|
46
|
+
MCP activity → API keys and add it to the `env` block to unlock write tools
|
|
47
|
+
(propose / link / grade).
|
|
48
|
+
|
|
43
49
|
Get your workspace ID from the dashboard's URL bar
|
|
44
50
|
(`dashboard.roadmapperai.com/settings` → workspace section) or from the
|
|
45
51
|
generated config block in Settings → Connect.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@roadmapperai/mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Roadmapper AI MCP server — exposes a planning surface (themes, capabilities, tasks, sprints, PRs) to coding agents via stdio JSON-RPC. Pairs with the Roadmapper AI workspace at dashboard.roadmapperai.com.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"mcp",
|
package/server.mjs
CHANGED
|
@@ -3,42 +3,50 @@
|
|
|
3
3
|
* Roadmapper MCP server — zero-dependency stdio JSON-RPC.
|
|
4
4
|
*
|
|
5
5
|
* Exposes a planning surface so an agent can read the roadmap and
|
|
6
|
-
* (when authorized) propose tasks or stamp acceptance grades
|
|
6
|
+
* (when authorized) propose tasks or stamp acceptance grades.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
8
|
+
* Customer-facing env vars (brand-named, no backend disclosure):
|
|
9
|
+
* ROADMAPPER_BACKEND_URL — backend project URL
|
|
10
|
+
* ROADMAPPER_PUBLISHABLE_KEY — public client key (RLS-scoped)
|
|
11
|
+
* ROADMAPPER_WORKSPACE_ID — target workspace
|
|
12
|
+
* ROADMAPPER_API_KEY — write auth (rmpr_… token from
|
|
13
|
+
* Settings → MCP activity → API keys)
|
|
14
|
+
* ROADMAPPER_BROKER_URL — optional override for the write
|
|
15
|
+
* broker; defaults to BACKEND_URL/
|
|
16
|
+
* functions/v1/mcp-broker
|
|
15
17
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* the seed exactly like the app does.
|
|
22
|
-
* 3. Writes require SUPABASE_SERVICE_ROLE_KEY (bypasses RLS). Without
|
|
23
|
-
* it the write tools return an error result and the read tools
|
|
24
|
-
* still work.
|
|
18
|
+
* Legacy operator-only env vars (still accepted for CI / maintainer
|
|
19
|
+
* local installs; not part of the customer-facing config):
|
|
20
|
+
* SUPABASE_URL / SUPABASE_PUBLISHABLE_KEY / SUPABASE_ANON_KEY /
|
|
21
|
+
* SUPABASE_WORKSPACE_ID / SUPABASE_SERVICE_ROLE_KEY ← operator
|
|
22
|
+
* ROADMAPPER_ADMIN_KEY — brand-named alias for SERVICE_ROLE_KEY
|
|
25
23
|
*
|
|
26
|
-
*
|
|
24
|
+
* Customer wire-up (Claude Code / Claude Desktop / Cursor):
|
|
27
25
|
* {
|
|
28
26
|
* "mcpServers": {
|
|
29
27
|
* "roadmapper": {
|
|
30
|
-
* "command": "
|
|
31
|
-
* "args": ["/
|
|
28
|
+
* "command": "npx",
|
|
29
|
+
* "args": ["-y", "@roadmapperai/mcp"],
|
|
32
30
|
* "env": {
|
|
33
|
-
* "
|
|
34
|
-
* "
|
|
35
|
-
* "
|
|
36
|
-
* "
|
|
31
|
+
* "ROADMAPPER_BACKEND_URL": "...",
|
|
32
|
+
* "ROADMAPPER_PUBLISHABLE_KEY": "sb_publishable_...",
|
|
33
|
+
* "ROADMAPPER_WORKSPACE_ID": "...",
|
|
34
|
+
* "ROADMAPPER_API_KEY": "rmpr_..." ← optional, enables writes
|
|
37
35
|
* }
|
|
38
36
|
* }
|
|
39
37
|
* }
|
|
40
38
|
* }
|
|
41
39
|
*
|
|
40
|
+
* Data sources, in order:
|
|
41
|
+
* 1. Local seed at src/data/roadmap.json (dev only; absent in npm
|
|
42
|
+
* installs, where readSeed() falls back to an empty roadmap and
|
|
43
|
+
* data loads from the backend).
|
|
44
|
+
* 2. Workspace edits via REST, when BACKEND_URL + PUBLISHABLE_KEY
|
|
45
|
+
* + WORKSPACE_ID are set.
|
|
46
|
+
* 3. Writes route through ROADMAPPER_API_KEY (customer path,
|
|
47
|
+
* validated server-side at the broker) or ROADMAPPER_ADMIN_KEY
|
|
48
|
+
* (operator path, bypasses RLS).
|
|
49
|
+
*
|
|
42
50
|
* Self-test: `node mcp/server.mjs --selftest` exercises every tool
|
|
43
51
|
* against the local seed and exits 0 on success, 1 on failure. Useful
|
|
44
52
|
* for verifying the install without an MCP client.
|
|
@@ -152,36 +160,190 @@ async function readAgentsMdForWorkspace() {
|
|
|
152
160
|
return readAgentsMd();
|
|
153
161
|
}
|
|
154
162
|
|
|
163
|
+
/**
|
|
164
|
+
* Per-workspace label cache for tool descriptions.
|
|
165
|
+
*
|
|
166
|
+
* When a workspace renames "Theme" → "Initiative" (Settings →
|
|
167
|
+
* Customize → Labels), the dashboard already shows the new label
|
|
168
|
+
* everywhere. The MCP server also surfaces level names — inside
|
|
169
|
+
* tool descriptions the agent reads. This cache holds the
|
|
170
|
+
* resolved labels for the session so the descriptions reflect
|
|
171
|
+
* the customer's vocabulary the moment the agent connects.
|
|
172
|
+
*
|
|
173
|
+
* Loaded lazily: first tools/list call kicks off the fetch
|
|
174
|
+
* (async, doesn't block the response — first agent sees defaults,
|
|
175
|
+
* subsequent calls see the customized labels). Cache is per
|
|
176
|
+
* process lifetime; restart the MCP server to pick up label
|
|
177
|
+
* changes. (Customers rarely change labels; a restart is fine.)
|
|
178
|
+
*/
|
|
179
|
+
const DEFAULT_TOOL_LABELS = Object.freeze({
|
|
180
|
+
theme: "theme",
|
|
181
|
+
themes: "themes",
|
|
182
|
+
capability: "capability",
|
|
183
|
+
capabilities: "capabilities",
|
|
184
|
+
sprint: "sprint",
|
|
185
|
+
sprints: "sprints",
|
|
186
|
+
task: "task",
|
|
187
|
+
tasks: "tasks",
|
|
188
|
+
});
|
|
189
|
+
let labelsCache = null;
|
|
190
|
+
let labelsLoadStarted = false;
|
|
191
|
+
|
|
192
|
+
function pluralizeForTools(s) {
|
|
193
|
+
if (!s) return s;
|
|
194
|
+
if (s.length > 1 && s.endsWith("s")) return s;
|
|
195
|
+
const m = s.match(/([^aeiouAEIOU])y$/);
|
|
196
|
+
if (m) return s.slice(0, -1) + "ies";
|
|
197
|
+
if (/(ch|sh|x|z)$/i.test(s)) return s + "es";
|
|
198
|
+
return s + "s";
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function normalizeFetchedLabels(raw) {
|
|
202
|
+
if (!raw || typeof raw !== "object") return DEFAULT_TOOL_LABELS;
|
|
203
|
+
const pick = (v, def) => {
|
|
204
|
+
const t = (v ?? "").trim();
|
|
205
|
+
return t.length === 0 ? def : t.toLowerCase();
|
|
206
|
+
};
|
|
207
|
+
const themeS = pick(raw.theme, "theme");
|
|
208
|
+
const capS = pick(raw.capability, "capability");
|
|
209
|
+
const sprintS = pick(raw.sprint, "sprint");
|
|
210
|
+
const taskS = pick(raw.task, "task");
|
|
211
|
+
return {
|
|
212
|
+
theme: themeS,
|
|
213
|
+
themes: pluralizeForTools(themeS),
|
|
214
|
+
capability: capS,
|
|
215
|
+
capabilities: pluralizeForTools(capS),
|
|
216
|
+
sprint: sprintS,
|
|
217
|
+
sprints: pluralizeForTools(sprintS),
|
|
218
|
+
task: taskS,
|
|
219
|
+
tasks: pluralizeForTools(taskS),
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
async function fetchWorkspaceLabels() {
|
|
224
|
+
const { apiKey, brokerUrl } = supabaseConfig();
|
|
225
|
+
if (!apiKey || !brokerUrl) return null;
|
|
226
|
+
try {
|
|
227
|
+
const res = await fetch(brokerUrl, {
|
|
228
|
+
method: "POST",
|
|
229
|
+
headers: {
|
|
230
|
+
Authorization: `Bearer ${apiKey}`,
|
|
231
|
+
"content-type": "application/json",
|
|
232
|
+
Accept: "application/json",
|
|
233
|
+
},
|
|
234
|
+
body: JSON.stringify({ rpc: "get_workspace_labels", body: {} }),
|
|
235
|
+
});
|
|
236
|
+
if (!res.ok) return null;
|
|
237
|
+
const parsed = await res.json();
|
|
238
|
+
return normalizeFetchedLabels(parsed);
|
|
239
|
+
} catch {
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/** Trigger label load once; safe to call repeatedly. */
|
|
245
|
+
function startLabelLoad() {
|
|
246
|
+
if (labelsLoadStarted) return;
|
|
247
|
+
labelsLoadStarted = true;
|
|
248
|
+
fetchWorkspaceLabels().then((l) => {
|
|
249
|
+
if (l) labelsCache = l;
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** Resolve labels currently in effect; defaults during load. */
|
|
254
|
+
function currentLabels() {
|
|
255
|
+
return labelsCache ?? DEFAULT_TOOL_LABELS;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Substitute level-name labels into a tool description.
|
|
260
|
+
*
|
|
261
|
+
* Two layers:
|
|
262
|
+
* 1. Explicit tokens — {theme}, {themes}, {capability}, etc. —
|
|
263
|
+
* always get replaced with the customer's label (or the
|
|
264
|
+
* default when nothing is customized).
|
|
265
|
+
* 2. Bare-word substitution — when a label differs from its
|
|
266
|
+
* default, every word-bounded occurrence of the default in
|
|
267
|
+
* the description is rewritten to the customized label.
|
|
268
|
+
* Preserves leading-cap for sentence starts ("Theme" →
|
|
269
|
+
* "Initiative", "theme" → "initiative"). Word boundaries
|
|
270
|
+
* keep compound identifiers (themeId, capabilityId) intact.
|
|
271
|
+
*
|
|
272
|
+
* Net: a customer who renames Theme → Initiative sees every tool
|
|
273
|
+
* description talk about "initiatives" instead of "themes", with
|
|
274
|
+
* zero manual changes to this file. A workspace that doesn't
|
|
275
|
+
* customize anything reads unchanged descriptions.
|
|
276
|
+
*/
|
|
277
|
+
function tplDescription(text, labels) {
|
|
278
|
+
let out = text.replace(
|
|
279
|
+
/\{(theme|themes|capability|capabilities|sprint|sprints|task|tasks)\}/g,
|
|
280
|
+
(_, k) => labels[k] ?? k
|
|
281
|
+
);
|
|
282
|
+
for (const [k, v] of Object.entries(labels)) {
|
|
283
|
+
const def = DEFAULT_TOOL_LABELS[k];
|
|
284
|
+
if (!def || v === def) continue;
|
|
285
|
+
out = out.replace(new RegExp(`\\b${def}\\b`, "g"), v);
|
|
286
|
+
const DefCap = def[0].toUpperCase() + def.slice(1);
|
|
287
|
+
const VCap = v[0].toUpperCase() + v.slice(1);
|
|
288
|
+
out = out.replace(new RegExp(`\\b${DefCap}\\b`, "g"), VCap);
|
|
289
|
+
}
|
|
290
|
+
return out;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Resolve a config value from a primary `ROADMAPPER_*` env var,
|
|
295
|
+
* falling back to a legacy `SUPABASE_*` alias when the primary
|
|
296
|
+
* isn't set. The brand-facing customer config block ships with
|
|
297
|
+
* the ROADMAPPER_* names so customers never see "supabase" in
|
|
298
|
+
* their MCP config; the SUPABASE_* aliases stay supported so
|
|
299
|
+
* existing operator / CI installs don't break on upgrade.
|
|
300
|
+
*/
|
|
301
|
+
function envEither(primary, ...fallbacks) {
|
|
302
|
+
for (const k of [primary, ...fallbacks]) {
|
|
303
|
+
const v = process.env[k];
|
|
304
|
+
if (v) return v;
|
|
305
|
+
}
|
|
306
|
+
return null;
|
|
307
|
+
}
|
|
308
|
+
|
|
155
309
|
/**
|
|
156
310
|
* The read key used to fetch the workspace row. Accepts the new
|
|
157
311
|
* publishable key (`sb_publishable_…`) or the legacy `anon`/JWT key.
|
|
158
312
|
*/
|
|
159
313
|
function readKey() {
|
|
160
|
-
return (
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
314
|
+
return envEither(
|
|
315
|
+
"ROADMAPPER_PUBLISHABLE_KEY",
|
|
316
|
+
"SUPABASE_PUBLISHABLE_KEY",
|
|
317
|
+
"SUPABASE_ANON_KEY"
|
|
164
318
|
);
|
|
165
319
|
}
|
|
166
320
|
|
|
167
321
|
function supabaseConfig() {
|
|
322
|
+
const url = envEither("ROADMAPPER_BACKEND_URL", "SUPABASE_URL");
|
|
168
323
|
return {
|
|
169
|
-
url
|
|
324
|
+
url,
|
|
170
325
|
readKey: readKey(),
|
|
171
|
-
|
|
172
|
-
|
|
326
|
+
// Operator path: a service-role-equivalent key. ROADMAPPER_ADMIN_KEY
|
|
327
|
+
// is the brand-facing name; SUPABASE_SERVICE_ROLE_KEY is the legacy
|
|
328
|
+
// alias used by CI and the maintainer's local workspace.
|
|
329
|
+
writeKey: envEither(
|
|
330
|
+
"ROADMAPPER_ADMIN_KEY",
|
|
331
|
+
"SUPABASE_SERVICE_ROLE_KEY"
|
|
332
|
+
),
|
|
333
|
+
workspaceId: envEither(
|
|
334
|
+
"ROADMAPPER_WORKSPACE_ID",
|
|
335
|
+
"SUPABASE_WORKSPACE_ID"
|
|
336
|
+
),
|
|
173
337
|
// ROADMAPPER_API_KEY is the customer-facing path: a per-workspace
|
|
174
338
|
// token (rmpr_…) minted from the dashboard. When set, write tools
|
|
175
339
|
// route through the mcp-broker Edge Function instead of needing
|
|
176
340
|
// a service-role key on the customer's machine. The broker URL
|
|
177
|
-
// defaults to the
|
|
178
|
-
//
|
|
341
|
+
// defaults to the project's edge endpoint but is overridable for
|
|
342
|
+
// self-hosted deployments / staging.
|
|
179
343
|
apiKey: process.env.ROADMAPPER_API_KEY || null,
|
|
180
344
|
brokerUrl:
|
|
181
345
|
process.env.ROADMAPPER_BROKER_URL ||
|
|
182
|
-
(
|
|
183
|
-
? `${process.env.SUPABASE_URL.replace(/\/$/, "")}/functions/v1/mcp-broker`
|
|
184
|
-
: null),
|
|
346
|
+
(url ? `${url.replace(/\/$/, "")}/functions/v1/mcp-broker` : null),
|
|
185
347
|
};
|
|
186
348
|
}
|
|
187
349
|
|
|
@@ -408,7 +570,7 @@ async function rpcCall(fn, body) {
|
|
|
408
570
|
// injects it before calling rpcCall so the override path works.
|
|
409
571
|
if (!url || !body?.p_workspace_id) {
|
|
410
572
|
throw new Error(
|
|
411
|
-
"Write tools require
|
|
573
|
+
"Write tools require ROADMAPPER_BACKEND_URL in env and a resolvable workspaceId (either ROADMAPPER_WORKSPACE_ID env or workspaceId arg)."
|
|
412
574
|
);
|
|
413
575
|
}
|
|
414
576
|
|
|
@@ -442,7 +604,7 @@ async function rpcCall(fn, body) {
|
|
|
442
604
|
// workspace; not what customers should ever configure.
|
|
443
605
|
if (!writeKey) {
|
|
444
606
|
throw new Error(
|
|
445
|
-
"Write tools require either ROADMAPPER_API_KEY (customer path) or
|
|
607
|
+
"Write tools require either ROADMAPPER_API_KEY (customer path) or ROADMAPPER_ADMIN_KEY (operator path)."
|
|
446
608
|
);
|
|
447
609
|
}
|
|
448
610
|
const res = await fetch(`${url}/rest/v1/rpc/${fn}`, {
|
|
@@ -3091,7 +3253,19 @@ async function handle(request) {
|
|
|
3091
3253
|
};
|
|
3092
3254
|
}
|
|
3093
3255
|
if (method === "tools/list") {
|
|
3094
|
-
|
|
3256
|
+
// Kick off the per-workspace label fetch (idempotent). The
|
|
3257
|
+
// first call serves descriptions with defaults; subsequent
|
|
3258
|
+
// calls in the same session see the customized labels once
|
|
3259
|
+
// the broker responds (typically <300ms). Most MCP clients
|
|
3260
|
+
// call tools/list once at connect and once after a reconnect,
|
|
3261
|
+
// so the timing usually works out.
|
|
3262
|
+
startLabelLoad();
|
|
3263
|
+
const labels = currentLabels();
|
|
3264
|
+
const tools = TOOLS.map((t) => ({
|
|
3265
|
+
...t,
|
|
3266
|
+
description: tplDescription(t.description, labels),
|
|
3267
|
+
}));
|
|
3268
|
+
return { jsonrpc: "2.0", id, result: { tools } };
|
|
3095
3269
|
}
|
|
3096
3270
|
if (method === "tools/call") {
|
|
3097
3271
|
const result = await callTool(params?.name, params?.arguments ?? {});
|