@roadmapperai/mcp 0.4.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/package.json +1 -1
- package/server.mjs +143 -1
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
|
@@ -160,6 +160,136 @@ async function readAgentsMdForWorkspace() {
|
|
|
160
160
|
return readAgentsMd();
|
|
161
161
|
}
|
|
162
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
|
+
|
|
163
293
|
/**
|
|
164
294
|
* Resolve a config value from a primary `ROADMAPPER_*` env var,
|
|
165
295
|
* falling back to a legacy `SUPABASE_*` alias when the primary
|
|
@@ -3123,7 +3253,19 @@ async function handle(request) {
|
|
|
3123
3253
|
};
|
|
3124
3254
|
}
|
|
3125
3255
|
if (method === "tools/list") {
|
|
3126
|
-
|
|
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 } };
|
|
3127
3269
|
}
|
|
3128
3270
|
if (method === "tools/call") {
|
|
3129
3271
|
const result = await callTool(params?.name, params?.arguments ?? {});
|