@roadmapperai/mcp 0.1.0 → 0.3.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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/server.mjs +103 -5
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@roadmapperai/mcp",
3
- "version": "0.1.0",
3
+ "version": "0.3.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
@@ -111,6 +111,47 @@ function readAgentsMd() {
111
111
  }
112
112
  }
113
113
 
114
+ /**
115
+ * Fetch a workspace-scoped rubric override via the mcp-broker.
116
+ *
117
+ * Returns the workspace's custom rubric content if they've authored
118
+ * one in the dashboard's Settings → Workspace customization tab,
119
+ * null if the workspace is on the default, or null on any error
120
+ * (we fall back to the bundled AGENTS.md rather than refuse to
121
+ * serve get_agents_md).
122
+ *
123
+ * Only called when ROADMAPPER_API_KEY is set — i.e. the customer
124
+ * install path. Operator path (SUPABASE_SERVICE_ROLE_KEY) skips
125
+ * this and always serves the bundled rubric.
126
+ */
127
+ async function fetchWorkspaceRubric() {
128
+ const { apiKey, brokerUrl } = supabaseConfig();
129
+ if (!apiKey || !brokerUrl) return null;
130
+ try {
131
+ const res = await fetch(brokerUrl, {
132
+ method: "POST",
133
+ headers: {
134
+ Authorization: `Bearer ${apiKey}`,
135
+ "content-type": "application/json",
136
+ Accept: "application/json",
137
+ },
138
+ body: JSON.stringify({ rpc: "get_workspace_rubric", body: {} }),
139
+ });
140
+ if (!res.ok) return null;
141
+ const parsed = await res.json();
142
+ if (typeof parsed === "string" && parsed.length > 0) return parsed;
143
+ return null;
144
+ } catch {
145
+ return null;
146
+ }
147
+ }
148
+
149
+ async function readAgentsMdForWorkspace() {
150
+ const custom = await fetchWorkspaceRubric();
151
+ if (custom) return custom;
152
+ return readAgentsMd();
153
+ }
154
+
114
155
  /**
115
156
  * The read key used to fetch the workspace row. Accepts the new
116
157
  * publishable key (`sb_publishable_…`) or the legacy `anon`/JWT key.
@@ -129,6 +170,18 @@ function supabaseConfig() {
129
170
  readKey: readKey(),
130
171
  writeKey: process.env.SUPABASE_SERVICE_ROLE_KEY || null,
131
172
  workspaceId: process.env.SUPABASE_WORKSPACE_ID || null,
173
+ // ROADMAPPER_API_KEY is the customer-facing path: a per-workspace
174
+ // token (rmpr_…) minted from the dashboard. When set, write tools
175
+ // route through the mcp-broker Edge Function instead of needing
176
+ // a service-role key on the customer's machine. The broker URL
177
+ // defaults to the public Supabase project's edge endpoint but is
178
+ // overridable for self-hosted deployments / staging.
179
+ apiKey: process.env.ROADMAPPER_API_KEY || null,
180
+ brokerUrl:
181
+ process.env.ROADMAPPER_BROKER_URL ||
182
+ (process.env.SUPABASE_URL
183
+ ? `${process.env.SUPABASE_URL.replace(/\/$/, "")}/functions/v1/mcp-broker`
184
+ : null),
132
185
  };
133
186
  }
134
187
 
@@ -350,12 +403,46 @@ function stripUndefined(o) {
350
403
  * function bodies.
351
404
  */
352
405
  async function rpcCall(fn, body) {
353
- const { url, writeKey } = supabaseConfig();
406
+ const { url, writeKey, apiKey, brokerUrl } = supabaseConfig();
354
407
  // body must already carry p_workspace_id — the per-tool resolver
355
408
  // injects it before calling rpcCall so the override path works.
356
- if (!url || !writeKey || !body?.p_workspace_id) {
409
+ if (!url || !body?.p_workspace_id) {
357
410
  throw new Error(
358
- "Write tools require SUPABASE_URL + SUPABASE_SERVICE_ROLE_KEY in env and a resolvable workspaceId (either SUPABASE_WORKSPACE_ID env or workspaceId arg)."
411
+ "Write tools require SUPABASE_URL in env and a resolvable workspaceId (either SUPABASE_WORKSPACE_ID env or workspaceId arg)."
412
+ );
413
+ }
414
+
415
+ // Customer path: ROADMAPPER_API_KEY routes through the mcp-broker
416
+ // Edge Function, which validates the key, hashes it against
417
+ // workspace_api_keys, and performs the RPC with service-role
418
+ // credentials never leaving the server side. This is what the
419
+ // published @roadmapperai/mcp package uses; customers' machines
420
+ // never see SUPABASE_SERVICE_ROLE_KEY.
421
+ if (apiKey && brokerUrl) {
422
+ const res = await fetch(brokerUrl, {
423
+ method: "POST",
424
+ headers: {
425
+ Authorization: `Bearer ${apiKey}`,
426
+ "content-type": "application/json",
427
+ Accept: "application/json",
428
+ },
429
+ body: JSON.stringify({ rpc: fn, body }),
430
+ });
431
+ if (!res.ok) {
432
+ const txt = await res.text();
433
+ throw new Error(
434
+ `mcp-broker rejected ${fn}: ${res.status} ${txt.slice(0, 300)}`
435
+ );
436
+ }
437
+ return res.json();
438
+ }
439
+
440
+ // Operator / dev path: SUPABASE_SERVICE_ROLE_KEY in env — bypasses
441
+ // RLS and the broker. Used in CI and the maintainer's local
442
+ // workspace; not what customers should ever configure.
443
+ if (!writeKey) {
444
+ throw new Error(
445
+ "Write tools require either ROADMAPPER_API_KEY (customer path) or SUPABASE_SERVICE_ROLE_KEY (operator path)."
359
446
  );
360
447
  }
361
448
  const res = await fetch(`${url}/rest/v1/rpc/${fn}`, {
@@ -1605,7 +1692,11 @@ async function callTool(name, args) {
1605
1692
  const fresh = session.rubricFetchedAt === null;
1606
1693
  session.rubricFetchedAt = Date.now();
1607
1694
  if (fresh) recordTelemetry("rubric_fetched", { via: "tool" }, wsId);
1608
- return textResult(readAgentsMd(), {
1695
+ // Customer path (ROADMAPPER_API_KEY set): ask the broker for
1696
+ // a workspace-scoped rubric. Falls back to the bundled
1697
+ // AGENTS.md when the workspace hasn't customized one.
1698
+ const rubric = await readAgentsMdForWorkspace();
1699
+ return textResult(rubric, {
1609
1700
  _meta: {
1610
1701
  roadmapper: {
1611
1702
  reminder:
@@ -2819,7 +2910,14 @@ async function readResource(uri) {
2819
2910
  }
2820
2911
  return {
2821
2912
  contents: [
2822
- { uri, mimeType: "text/markdown", text: readAgentsMd() },
2913
+ {
2914
+ uri,
2915
+ mimeType: "text/markdown",
2916
+ // Resource handler is also async, so the workspace
2917
+ // rubric override applies here too — same fallback chain
2918
+ // as the get_agents_md tool.
2919
+ text: await readAgentsMdForWorkspace(),
2920
+ },
2823
2921
  ],
2824
2922
  };
2825
2923
  }