@roadmapperai/mcp 0.9.5 → 0.9.6
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/AGENTS.md +54 -37
- package/README.md +9 -0
- package/package.json +1 -1
- package/server.mjs +134 -5
package/AGENTS.md
CHANGED
|
@@ -131,8 +131,8 @@ you do, every write tool below returns a structured error:
|
|
|
131
131
|
```json
|
|
132
132
|
{
|
|
133
133
|
"error": "prerequisite_missing",
|
|
134
|
-
"message": "Call get_agents_md first this session, then retry ...",
|
|
135
|
-
"fix": "get_agents_md
|
|
134
|
+
"message": "Call roadmap({ op: \"get_agents_md\" }) first this session, then retry ...",
|
|
135
|
+
"fix": "roadmap({ op: \"get_agents_md\" })"
|
|
136
136
|
}
|
|
137
137
|
```
|
|
138
138
|
|
|
@@ -275,15 +275,15 @@ the first such write is **refused** with a structured error:
|
|
|
275
275
|
{
|
|
276
276
|
"error": "repo_unmapped",
|
|
277
277
|
"message": "\"owner/name\" isn't mapped to a workspace ...",
|
|
278
|
-
"fix": "link_repo
|
|
279
|
-
"alt": "
|
|
278
|
+
"fix": "roadmap({ op: \"link_repo\" })",
|
|
279
|
+
"alt": "roadmap({ op: \"<op>\", args: { workspaceId: \"<target>\", ... } })"
|
|
280
280
|
}
|
|
281
281
|
```
|
|
282
282
|
|
|
283
283
|
Resolve it one of two ways, then retry:
|
|
284
|
-
- **`link_repo
|
|
285
|
-
workspace, so every future session resolves silently.
|
|
286
|
-
the right move when the repo *should* feed this workspace.
|
|
284
|
+
- **`roadmap({ op: "link_repo" })`** — maps the repo you're in to
|
|
285
|
+
your key's workspace, so every future session resolves silently.
|
|
286
|
+
This is the right move when the repo *should* feed this workspace.
|
|
287
287
|
- **Pass `workspaceId` explicitly** on the call — proceeds without
|
|
288
288
|
mapping the repo. This is the escape hatch when you're working
|
|
289
289
|
across **several repos in one session** and just want this write
|
|
@@ -356,52 +356,69 @@ PRs without submitted acceptance grades; call submit_acceptance_grades
|
|
|
356
356
|
for TK-X, TK-Y, TK-Z." Surface these to the user; they're the
|
|
357
357
|
roadmap's way of asking for the next action.
|
|
358
358
|
|
|
359
|
-
|
|
359
|
+
Two install shapes:
|
|
360
|
+
|
|
361
|
+
- **Customer (recommended):** `npx -y @roadmapperai/mcp` with
|
|
362
|
+
`ROADMAPPER_BACKEND_URL` + `ROADMAPPER_PUBLISHABLE_KEY` +
|
|
363
|
+
`ROADMAPPER_WORKSPACE_ID`, plus `ROADMAPPER_API_KEY` (an `rmpr_…` key)
|
|
364
|
+
to enable writes. Writes route through the mcp-broker, so no
|
|
365
|
+
service-role key ever lives on the machine. The dashboard's
|
|
366
|
+
Settings → Connect page generates this block pre-filled.
|
|
367
|
+
- **Self-host / operator:** run `node mcp/server.mjs` from a local
|
|
368
|
+
checkout with the `SUPABASE_*` env vars (legacy aliases the server
|
|
369
|
+
still accepts) and the Postgres migrations applied for writes.
|
|
370
|
+
|
|
371
|
+
Capability tiers, by which env vars are set (either shape):
|
|
360
372
|
|
|
361
373
|
| Tier | Required env | What you get |
|
|
362
374
|
|-------------------|-----------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------|
|
|
363
375
|
| Seed-only | none | Read tools against the static `roadmap.json`. |
|
|
364
|
-
| Live read | `
|
|
365
|
-
| Live read + write |
|
|
366
|
-
|
|
367
|
-
If a write
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
Sanity check
|
|
372
|
-
every
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
the
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
you
|
|
382
|
-
|
|
383
|
-
|
|
376
|
+
| Live read | `ROADMAPPER_BACKEND_URL` + `ROADMAPPER_PUBLISHABLE_KEY` + `ROADMAPPER_WORKSPACE_ID` (or the `SUPABASE_*` equivalents) | Read merged with the workspace's edits. |
|
|
377
|
+
| Live read + write | the above plus `ROADMAPPER_API_KEY` (`rmpr_…`, broker path) — or, self-hosting, `SUPABASE_SERVICE_ROLE_KEY` with migrations applied | Read + propose_* / grade / archive_* / unarchive_* / move_* / update_* ops. |
|
|
378
|
+
|
|
379
|
+
If a write returns an error about a missing key, you're on the read
|
|
380
|
+
tier; don't keep retrying — tell the human what you'd do and let them
|
|
381
|
+
apply it.
|
|
382
|
+
|
|
383
|
+
Sanity check (repo checkout only): `node mcp/server.mjs --selftest`
|
|
384
|
+
runs every check against the bundled seed and prints a pass/fail
|
|
385
|
+
summary. Run it from the **repo root** — an npm/npx install ships no
|
|
386
|
+
seed file, so ~13 seed-dependent checks fail with `ENOENT` there and
|
|
387
|
+
that is EXPECTED, not a broken install; the server still works over
|
|
388
|
+
the wire (reads fall back to a demo seed; live edits come from the
|
|
389
|
+
backend).
|
|
390
|
+
|
|
391
|
+
If the operator chose project-scoped install (`.mcp.json` in the repo,
|
|
392
|
+
the default `npm run mcp:setup` path), the MCP only loads when their
|
|
393
|
+
client is launched from that repo. If you're an agent in another
|
|
394
|
+
codebase and the `roadmapper` tools aren't visible, ask the operator
|
|
395
|
+
to either (a) merge the `mcpServers.roadmapper` block into their
|
|
396
|
+
user-level client config or (b) point you at the roadmapper repo.
|
|
397
|
+
|
|
398
|
+
Wire-up (Claude Code, Claude Desktop, or any MCP client) — customer path:
|
|
384
399
|
|
|
385
400
|
```jsonc
|
|
386
401
|
{
|
|
387
402
|
"mcpServers": {
|
|
388
403
|
"roadmapper": {
|
|
389
|
-
"command": "
|
|
390
|
-
"args": ["/
|
|
404
|
+
"command": "npx",
|
|
405
|
+
"args": ["-y", "@roadmapperai/mcp"],
|
|
391
406
|
"env": {
|
|
392
|
-
"
|
|
393
|
-
"
|
|
394
|
-
"
|
|
395
|
-
"
|
|
407
|
+
"ROADMAPPER_BACKEND_URL": "https://<id>.supabase.co",
|
|
408
|
+
"ROADMAPPER_PUBLISHABLE_KEY": "sb_publishable_...",
|
|
409
|
+
"ROADMAPPER_WORKSPACE_ID": "<workspace id>",
|
|
410
|
+
"ROADMAPPER_API_KEY": "<rmpr_… — only if write ops wanted>"
|
|
396
411
|
}
|
|
397
412
|
}
|
|
398
413
|
}
|
|
399
414
|
}
|
|
400
415
|
```
|
|
401
416
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
417
|
+
Self-hosting from a checkout instead? Use `"command": "node"` with the
|
|
418
|
+
absolute path to `mcp/server.mjs`; the `SUPABASE_*` env names are
|
|
419
|
+
accepted as aliases. The `env` block is optional — drop it to serve the
|
|
420
|
+
seed only. See [README.md](/README.md#mcp-server) for the config-file
|
|
421
|
+
path per client and the full env-var matrix.
|
|
405
422
|
|
|
406
423
|
## The snapshot file in connected repos
|
|
407
424
|
|
package/README.md
CHANGED
|
@@ -150,6 +150,15 @@ the check with `ROADMAPPER_DISABLE_UPDATE_CHECK=1`.
|
|
|
150
150
|
|
|
151
151
|
### Recent changes
|
|
152
152
|
|
|
153
|
+
- **0.9.6** — hardening pass on top of the 0.9.5 collapse. `submit_acceptance_grades`
|
|
154
|
+
now rejects a negative/non-integer/out-of-range `index`, a non-array `grades`,
|
|
155
|
+
and a status that isn't `pass`/`fail` (the per-op schema is no longer on the
|
|
156
|
+
wire for clients to enforce, so the handler validates). A task can no longer be
|
|
157
|
+
moved to `in_progress` with an empty acceptance list on the MCP write path.
|
|
158
|
+
`propose_theme` with `dryRun` now previews a near-duplicate (with a warning)
|
|
159
|
+
instead of hard-blocking. Server instructions no longer tell agents to proceed
|
|
160
|
+
only on `status:"resolved"` (the normal env install resolves to `env_default`).
|
|
161
|
+
Docs reconciled to the dispatch surface + customer (npx) install path.
|
|
153
162
|
- **0.9.5** — **tool-surface collapse for token efficiency.** `tools/list` now
|
|
154
163
|
advertises three dispatch tools (`roadmap_search` / `roadmap_describe` /
|
|
155
164
|
`roadmap`) instead of ~34, cutting the always-loaded tool definitions ~97%
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@roadmapperai/mcp",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.6",
|
|
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
|
@@ -511,7 +511,7 @@ PER-SESSION WORKFLOW
|
|
|
511
511
|
1. Orient first: get_roadmap_snapshot (or list_themes / list_capabilities). This also satisfies the discovery gates below.
|
|
512
512
|
2. Writing requires the rubric: every workspace-mutating tool (propose_*/update_*/archive_*/unarchive_*/move_*/record_outcome_reading/link_pr/submit_acceptance_grades) refuses until you call get_agents_md once this session (reading roadmapper://rubric also counts).
|
|
513
513
|
3. Reuse before creating: suggest_capability_for({description}) to find an existing home; only propose a new capability if nothing fits. suggest_theme_for / list_themes before proposing a theme.
|
|
514
|
-
4. Before your first write, call get_active_workspace
|
|
514
|
+
4. Before your first write, call get_active_workspace. Proceed if status is "resolved", or if it's "env_default" and the named workspace is the one you intend (the common, correct case for an env-configured install — its \`next.detail\` says how to confirm). Stop and follow the \`next\` action only for "ambiguous" or "unresolved" (e.g. link_repo), so writes don't land in the wrong workspace.
|
|
515
515
|
5. dryRun:true validates any write without committing. Reference everything by stable ID, never by name.
|
|
516
516
|
|
|
517
517
|
MODEL (don't conflate the layers)
|
|
@@ -3800,7 +3800,9 @@ async function proposeTheme(args, projected, wsId) {
|
|
|
3800
3800
|
nearest = t;
|
|
3801
3801
|
}
|
|
3802
3802
|
}
|
|
3803
|
-
|
|
3803
|
+
// dryRun is exempt (like the autonomy gate below) so a validate-only call
|
|
3804
|
+
// always returns a preview; the overlap is surfaced as a warning instead.
|
|
3805
|
+
if (nearest && nearestScore >= THEME_SPRAWL_BLOCK && args.force !== true && !args.dryRun) {
|
|
3804
3806
|
return textResult(
|
|
3805
3807
|
JSON.stringify(
|
|
3806
3808
|
{
|
|
@@ -3860,13 +3862,19 @@ async function proposeTheme(args, projected, wsId) {
|
|
|
3860
3862
|
};
|
|
3861
3863
|
|
|
3862
3864
|
if (args.dryRun) {
|
|
3865
|
+
const warnings =
|
|
3866
|
+
nearest && nearestScore >= THEME_SPRAWL_BLOCK && args.force !== true
|
|
3867
|
+
? [
|
|
3868
|
+
`Overlaps existing theme ${nearest.id} (${nearest.name}) at ${nearestScore.toFixed(2)} (block bar ${THEME_SPRAWL_BLOCK}). A real call would be refused as too_similar unless force:true — prefer filing under it via ${opCall("propose_capability", `{ pillarId: "${nearest.id}", ... }`)}.`,
|
|
3869
|
+
]
|
|
3870
|
+
: [];
|
|
3863
3871
|
return textResult(
|
|
3864
3872
|
JSON.stringify(
|
|
3865
3873
|
{
|
|
3866
3874
|
ok: true,
|
|
3867
3875
|
dryRun: true,
|
|
3868
3876
|
wouldCreate: theme,
|
|
3869
|
-
warnings
|
|
3877
|
+
warnings,
|
|
3870
3878
|
message: `Would create theme ${id} (${name}). No record written.`,
|
|
3871
3879
|
},
|
|
3872
3880
|
null,
|
|
@@ -4643,6 +4651,28 @@ async function updateEntity(kind, args, wsId, projected) {
|
|
|
4643
4651
|
);
|
|
4644
4652
|
}
|
|
4645
4653
|
|
|
4654
|
+
// Acceptance gate (MCP-path stopgap). The SQL gate added in migration 0096
|
|
4655
|
+
// ("a task can't transition to in_progress without >=1 acceptance criterion")
|
|
4656
|
+
// landed on a different update_entity overload than the one this JS path
|
|
4657
|
+
// calls (an overload collision — the durable fix is a SQL migration that
|
|
4658
|
+
// reconciles them). Enforce it here too so the rule holds on the MCP path:
|
|
4659
|
+
// we have the merged `current` and the patch, so we know the post-update
|
|
4660
|
+
// acceptance and the real status transition.
|
|
4661
|
+
if (
|
|
4662
|
+
kind === "task" &&
|
|
4663
|
+
effectivePatch.status === "in_progress" &&
|
|
4664
|
+
current.status !== "in_progress"
|
|
4665
|
+
) {
|
|
4666
|
+
const accAfter = Array.isArray(cleanedPatch.acceptance)
|
|
4667
|
+
? cleanedPatch.acceptance
|
|
4668
|
+
: current.acceptance;
|
|
4669
|
+
if (!Array.isArray(accAfter) || accAfter.length === 0) {
|
|
4670
|
+
return errorResult(
|
|
4671
|
+
"Cannot move a task to in_progress without at least one acceptance criterion — add acceptance in the same patch (an empty acceptance list is a stop signal)."
|
|
4672
|
+
);
|
|
4673
|
+
}
|
|
4674
|
+
}
|
|
4675
|
+
|
|
4646
4676
|
try {
|
|
4647
4677
|
const result = await rpcCall("update_entity", {
|
|
4648
4678
|
p_workspace_id: wsId,
|
|
@@ -5010,10 +5040,27 @@ async function submitAcceptanceGrades(args, projected, wsId) {
|
|
|
5010
5040
|
return errorResult(
|
|
5011
5041
|
`Task ${task.id} has no acceptance criteria to grade. Add some first.`
|
|
5012
5042
|
);
|
|
5043
|
+
// Validate every grade BEFORE the RPC. The per-op inputSchema
|
|
5044
|
+
// (index integer >= 0, status enum) is advisory only — nothing enforces it
|
|
5045
|
+
// server-side, and since the tool-surface collapse the schema isn't even on
|
|
5046
|
+
// the wire for a client to check. So guard here: a non-array drops to an
|
|
5047
|
+
// opaque -32603 from the for-of, and a negative/float index reaches the SQL
|
|
5048
|
+
// jsonb_set with Postgres negative-index-from-end semantics, silently
|
|
5049
|
+
// overwriting an UNRELATED criterion's grade.
|
|
5050
|
+
if (!Array.isArray(args.grades) || args.grades.length === 0)
|
|
5051
|
+
return errorResult(
|
|
5052
|
+
"grades must be a non-empty array of { index, status, note? } objects."
|
|
5053
|
+
);
|
|
5013
5054
|
for (const g of args.grades) {
|
|
5014
|
-
if (g
|
|
5055
|
+
if (!g || typeof g !== "object" || Array.isArray(g))
|
|
5056
|
+
return errorResult("each grade must be an object { index, status, note? }.");
|
|
5057
|
+
if (!Number.isInteger(g.index) || g.index < 0 || g.index >= max)
|
|
5015
5058
|
return errorResult(
|
|
5016
|
-
`Grade index ${g.index} is
|
|
5059
|
+
`Grade index ${JSON.stringify(g.index)} is invalid — must be an integer in 0..${max - 1} (task has ${max} criteria).`
|
|
5060
|
+
);
|
|
5061
|
+
if (g.status !== "pass" && g.status !== "fail")
|
|
5062
|
+
return errorResult(
|
|
5063
|
+
`Grade status for index ${g.index} must be "pass" or "fail" (got ${JSON.stringify(g.status)}).`
|
|
5017
5064
|
);
|
|
5018
5065
|
}
|
|
5019
5066
|
|
|
@@ -7193,6 +7240,88 @@ async function runSelftest() {
|
|
|
7193
7240
|
}),
|
|
7194
7241
|
pass: (r) => r?.result?.isError === true,
|
|
7195
7242
|
},
|
|
7243
|
+
{
|
|
7244
|
+
// submit_acceptance_grades: a negative index must be rejected up front —
|
|
7245
|
+
// otherwise it reaches the SQL jsonb_set and (negative-index-from-end)
|
|
7246
|
+
// overwrites an UNRELATED criterion's grade. Direct call so the test
|
|
7247
|
+
// targets the validator, not the rubric/seed gates.
|
|
7248
|
+
name: "submit_acceptance_grades rejects a negative index",
|
|
7249
|
+
fn: () =>
|
|
7250
|
+
submitAcceptanceGrades(
|
|
7251
|
+
{ taskId: "TK-G", grades: [{ index: -1, status: "pass" }] },
|
|
7252
|
+
{ tasks: [{ id: "TK-G", acceptance: ["a", "b"] }], capabilities: [], themes: [] },
|
|
7253
|
+
"ws-test"
|
|
7254
|
+
),
|
|
7255
|
+
pass: (r) =>
|
|
7256
|
+
r?.isError === true && (r?.content?.[0]?.text ?? "").includes("invalid"),
|
|
7257
|
+
},
|
|
7258
|
+
{
|
|
7259
|
+
name: "submit_acceptance_grades rejects a non-array grades arg",
|
|
7260
|
+
fn: () =>
|
|
7261
|
+
submitAcceptanceGrades(
|
|
7262
|
+
{ taskId: "TK-G", grades: "not-an-array" },
|
|
7263
|
+
{ tasks: [{ id: "TK-G", acceptance: ["a", "b"] }], capabilities: [], themes: [] },
|
|
7264
|
+
"ws-test"
|
|
7265
|
+
),
|
|
7266
|
+
pass: (r) =>
|
|
7267
|
+
r?.isError === true &&
|
|
7268
|
+
(r?.content?.[0]?.text ?? "").includes("non-empty array"),
|
|
7269
|
+
},
|
|
7270
|
+
{
|
|
7271
|
+
name: "submit_acceptance_grades rejects a status that isn't pass/fail",
|
|
7272
|
+
fn: () =>
|
|
7273
|
+
submitAcceptanceGrades(
|
|
7274
|
+
{ taskId: "TK-G", grades: [{ index: 0, status: "maybe" }] },
|
|
7275
|
+
{ tasks: [{ id: "TK-G", acceptance: ["a", "b"] }], capabilities: [], themes: [] },
|
|
7276
|
+
"ws-test"
|
|
7277
|
+
),
|
|
7278
|
+
pass: (r) =>
|
|
7279
|
+
r?.isError === true && (r?.content?.[0]?.text ?? "").includes("pass"),
|
|
7280
|
+
},
|
|
7281
|
+
{
|
|
7282
|
+
// MCP-path stopgap for the 0096 SQL-overload gate: a task can't move to
|
|
7283
|
+
// in_progress with an empty acceptance list. Direct call with a synthetic
|
|
7284
|
+
// empty-acceptance task (the gate fires before any RPC).
|
|
7285
|
+
name: "update_task in_progress gate blocks a task with no acceptance",
|
|
7286
|
+
fn: () =>
|
|
7287
|
+
updateEntity(
|
|
7288
|
+
"task",
|
|
7289
|
+
{ taskId: "TK-EMPTYACC", patch: { status: "in_progress" }, reason: "starting" },
|
|
7290
|
+
"ws-test",
|
|
7291
|
+
{
|
|
7292
|
+
tasks: [{ id: "TK-EMPTYACC", status: "planned", acceptance: [] }],
|
|
7293
|
+
capabilities: [],
|
|
7294
|
+
themes: [],
|
|
7295
|
+
}
|
|
7296
|
+
),
|
|
7297
|
+
pass: (r) =>
|
|
7298
|
+
r?.isError === true && (r?.content?.[0]?.text ?? "").includes("acceptance"),
|
|
7299
|
+
},
|
|
7300
|
+
{
|
|
7301
|
+
// propose_theme dryRun must PREVIEW a near-duplicate (with a warning),
|
|
7302
|
+
// not hard-block — identical tokens force jaccard >= block bar.
|
|
7303
|
+
name: "propose_theme dryRun previews a near-duplicate with a warning",
|
|
7304
|
+
fn: () =>
|
|
7305
|
+
proposeTheme(
|
|
7306
|
+
{ name: "Duplicate Pillar Name", description: "identical tokens here", dryRun: true },
|
|
7307
|
+
{
|
|
7308
|
+
themes: [{ id: "TH-DUP", name: "Duplicate Pillar Name", description: "identical tokens here" }],
|
|
7309
|
+
capabilities: [],
|
|
7310
|
+
tasks: [],
|
|
7311
|
+
settings: {},
|
|
7312
|
+
},
|
|
7313
|
+
"ws-test"
|
|
7314
|
+
),
|
|
7315
|
+
pass: (r) => {
|
|
7316
|
+
if (r?.isError) return false;
|
|
7317
|
+
try {
|
|
7318
|
+
const b = JSON.parse(r?.content?.[0]?.text ?? "{}");
|
|
7319
|
+
return b.dryRun === true && Array.isArray(b.warnings) && b.warnings.length > 0;
|
|
7320
|
+
} catch {
|
|
7321
|
+
return false;
|
|
7322
|
+
}
|
|
7323
|
+
},
|
|
7324
|
+
},
|
|
7196
7325
|
{
|
|
7197
7326
|
// The three update ops are reachable + describable via the dispatch
|
|
7198
7327
|
// surface (not advertised by name in tools/list anymore).
|