@cr8rcho/alkahest 0.1.76 → 0.1.77

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.
@@ -14,46 +14,66 @@ import { listProjects } from "../core/listProjects.js";
14
14
  import { listHistory } from "../core/history.js";
15
15
  import { findProjectRoot } from "../core/project.js";
16
16
  import { checkForUpdate, cachedUpdateStatus } from "../core/version.js";
17
+ import { fetchPublishedMap } from "../core/mapFetch.js";
18
+ // Re-exported so the hosted repo's /api/mcp route needs no direct SDK dependency (ADR-095, hosted repo).
19
+ export { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
17
20
  const require = createRequire(import.meta.url);
18
21
  const pkg = require("../../package.json");
19
- /**
20
- * MCP server that lets agents (Claude Code/Codex/Cursor) query the product map (ALKAHEST.md §7).
21
- * No LLM key required — reasoning is done by the calling agent. Tools provide only deterministic structure.
22
- * Default target is the server's working directory (cwd). Each tool's `path` can point to a different project.
23
- */
24
- export function buildServer() {
22
+ export function buildServer(remote) {
25
23
  const server = new McpServer({ name: "alkahest", version: pkg.version });
26
24
  const rootOf = (path) => resolve(path ?? process.cwd());
27
- server.registerTool("scan", {
28
- title: "Scan project",
29
- description: "Statically analyze a React/Next project to create/update a product map (.alkahest/map.json). " +
30
- "Extracts screens, transitions between screens, and the API/data calls each screen makes. Returns a result summary (counts). " +
31
- "The map is viewed on the hosted viewer — run the publish tool for a shareable link.",
32
- inputSchema: { path: z.string().optional().describe("Project root (default: cwd)") },
33
- }, async ({ path }) => {
34
- const result = runScan(rootOf(path));
35
- if (!result)
36
- return text("No screens found. Only Next app-router (page.* under app/ or src/app/) is supported.");
37
- const m = result.map;
38
- return json({
39
- framework: m.meta.framework,
40
- router: m.meta.router,
41
- screens: m.screens.length,
42
- resources: m.resources.length,
43
- transitions: m.transitions.length,
44
- calls: m.calls.length,
45
- mapPath: result.outFile,
25
+ /** Thread the connector's token/api into a core call's params (no-op for local stdio). */
26
+ const withAuth = (params) => remote ? { ...params, token: remote.token, api: remote.api } : params;
27
+ /** The product map: the local checkout's map.json, or (remote) the published bytes. */
28
+ const getMap = async (path, project, mapSlug) => remote ? fetchPublishedMap({ api: remote.api, token: remote.token, project, mapSlug }) : loadOrScan(rootOf(path));
29
+ const noMapMsg = remote
30
+ ? "No published code map. Pass `project` (a slug from list_projects), and `map` when the project has several code maps."
31
+ : "No screens, or unsupported project.";
32
+ /** Remote connectors have no env/config to edit — steer those hints to the connector URL + `project` arg. */
33
+ const hinted = (local) => remote
34
+ ? {
35
+ ...local,
36
+ no_token: "The connector URL is missing its token — recreate it at alkahest.app → API tokens.",
37
+ no_api: "The connector's API base is misconfigured.",
38
+ no_slug: "Pass `project` — a slug from list_projects.",
39
+ invalid_token: "The token in the connector URL is invalid or revoked — create a new one at alkahest.app → API tokens and update the connector.",
40
+ }
41
+ : local;
42
+ if (!remote)
43
+ server.registerTool("scan", {
44
+ title: "Scan project",
45
+ description: "Statically analyze a React/Next project to create/update a product map (.alkahest/map.json). " +
46
+ "Extracts screens, transitions between screens, and the API/data calls each screen makes. Returns a result summary (counts). " +
47
+ "The map is viewed on the hosted viewer — run the publish tool for a shareable link.",
48
+ inputSchema: { path: z.string().optional().describe("Project root (default: cwd)") },
49
+ }, async ({ path }) => {
50
+ const result = runScan(rootOf(path));
51
+ if (!result)
52
+ return text("No screens found. Only Next app-router (page.* under app/ or src/app/) is supported.");
53
+ const m = result.map;
54
+ return json({
55
+ framework: m.meta.framework,
56
+ router: m.meta.router,
57
+ screens: m.screens.length,
58
+ resources: m.resources.length,
59
+ transitions: m.transitions.length,
60
+ calls: m.calls.length,
61
+ mapPath: result.outFile,
62
+ });
46
63
  });
47
- });
48
64
  server.registerTool("overview", {
49
65
  title: "Product map overview",
50
66
  description: "Full product map overview: list of screens (route/title/feature count) and list of resources (label/number of calling screens). " +
51
67
  "Auto-scans if map.json is missing. Call this first to grasp the product structure at a glance.",
52
- inputSchema: { path: z.string().optional() },
53
- }, async ({ path }) => {
54
- const map = loadOrScan(rootOf(path));
68
+ inputSchema: {
69
+ path: z.string().optional(),
70
+ project: z.string().optional().describe("Which project (slug) — remote connectors read the PUBLISHED code map (slugs from list_projects). Ignored locally."),
71
+ map: z.string().optional().describe("Which code map when the project has several (remote connectors only; default: the oldest)"),
72
+ },
73
+ }, async ({ path, project, map: mapSlug }) => {
74
+ const map = await getMap(path, project, mapSlug);
55
75
  if (!map)
56
- return text("No screens, or unsupported project.");
76
+ return text(noMapMsg);
57
77
  return json({
58
78
  framework: map.meta.framework,
59
79
  router: map.meta.router,
@@ -76,11 +96,16 @@ export function buildServer() {
76
96
  title: "Screen detail",
77
97
  description: "Full structure of one screen: UI features, outgoing/incoming transitions, called resources (API/data), components, and source location. " +
78
98
  "The agent can use this data to write a summary or PRD itself. Specify the screen by id/route/title.",
79
- inputSchema: { screen: z.string().describe("screen id / route / title"), path: z.string().optional() },
80
- }, async ({ screen, path }) => {
81
- const map = loadOrScan(rootOf(path));
99
+ inputSchema: {
100
+ screen: z.string().describe("screen id / route / title"),
101
+ path: z.string().optional(),
102
+ project: z.string().optional().describe("Which project (slug) — remote connectors read the PUBLISHED code map (slugs from list_projects). Ignored locally."),
103
+ map: z.string().optional().describe("Which code map when the project has several (remote connectors only; default: the oldest)"),
104
+ },
105
+ }, async ({ screen, path, project, map: mapSlug }) => {
106
+ const map = await getMap(path, project, mapSlug);
82
107
  if (!map)
83
- return text("No screens, or unsupported project.");
108
+ return text(noMapMsg);
84
109
  const s = matchScreen(map, screen);
85
110
  if (!s)
86
111
  return text(`Screen not found: ${screen}`);
@@ -90,11 +115,16 @@ export function buildServer() {
90
115
  title: "Resource callers (impact)",
91
116
  description: "Returns the screens that call a specific resource (API endpoint/data). For understanding data dependencies and change impact. " +
92
117
  "Specify the resource by id ('GET /api/orders') or a path fragment ('/api/orders').",
93
- inputSchema: { resource: z.string(), path: z.string().optional() },
94
- }, async ({ resource, path }) => {
95
- const map = loadOrScan(rootOf(path));
118
+ inputSchema: {
119
+ resource: z.string(),
120
+ path: z.string().optional(),
121
+ project: z.string().optional().describe("Which project (slug) — remote connectors read the PUBLISHED code map (slugs from list_projects). Ignored locally."),
122
+ map: z.string().optional().describe("Which code map when the project has several (remote connectors only; default: the oldest)"),
123
+ },
124
+ }, async ({ resource, path, project, map: mapSlug }) => {
125
+ const map = await getMap(path, project, mapSlug);
96
126
  if (!map)
97
- return text("No screens, or unsupported project.");
127
+ return text(noMapMsg);
98
128
  const q = resource.toLowerCase();
99
129
  const matched = map.resources.filter((r) => r.id.toLowerCase() === q || (r.path ?? "").toLowerCase().includes(q) || r.label.toLowerCase().includes(q));
100
130
  return json(matched.map((r) => ({
@@ -105,108 +135,112 @@ export function buildServer() {
105
135
  })));
106
136
  });
107
137
  // ---- write-back tools: the agent saves its prose into map.json; publish shows it on the hosted viewer ----
108
- server.registerTool("set_summary", {
109
- title: "Set screen summary",
110
- description: "Save a one-line, PM-friendly summary ('what the user does here') onto a screen in map.json — it appears " +
111
- "in the screen's panel on the hosted viewer after the next publish. Write the summary yourself from get_screen data.",
112
- inputSchema: {
113
- screen: z.string().describe("screen id / route / title"),
114
- summary: z.string().describe("a 1-2 sentence summary in the user's language"),
115
- path: z.string().optional(),
116
- },
117
- }, async ({ screen, summary, path }) => writeField(rootOf(path), screen, (s) => { s.summary = summary; }));
118
- server.registerTool("set_prd", {
119
- title: "Set screen PRD",
120
- description: "Save a PRD/requirements markdown onto a screen in map.json — it appears in the screen's panel on the " +
121
- "hosted viewer (rendered) after the next publish. Write the PRD yourself from get_screen / who_calls data.",
122
- inputSchema: {
123
- screen: z.string().describe("screen id / route / title"),
124
- prd: z.string().describe("PRD/requirements as markdown"),
125
- path: z.string().optional(),
126
- },
127
- }, async ({ screen, prd, path }) => writeField(rootOf(path), screen, (s) => { s.prd = prd; }));
138
+ if (!remote)
139
+ server.registerTool("set_summary", {
140
+ title: "Set screen summary",
141
+ description: "Save a one-line, PM-friendly summary ('what the user does here') onto a screen in map.json it appears " +
142
+ "in the screen's panel on the hosted viewer after the next publish. Write the summary yourself from get_screen data.",
143
+ inputSchema: {
144
+ screen: z.string().describe("screen id / route / title"),
145
+ summary: z.string().describe("a 1-2 sentence summary in the user's language"),
146
+ path: z.string().optional(),
147
+ },
148
+ }, async ({ screen, summary, path }) => writeField(rootOf(path), screen, (s) => { s.summary = summary; }));
149
+ if (!remote)
150
+ server.registerTool("set_prd", {
151
+ title: "Set screen PRD",
152
+ description: "Save a PRD/requirements markdown onto a screen in map.json — it appears in the screen's panel on the " +
153
+ "hosted viewer (rendered) after the next publish. Write the PRD yourself from get_screen / who_calls data.",
154
+ inputSchema: {
155
+ screen: z.string().describe("screen id / route / title"),
156
+ prd: z.string().describe("PRD/requirements as markdown"),
157
+ path: z.string().optional(),
158
+ },
159
+ }, async ({ screen, prd, path }) => writeField(rootOf(path), screen, (s) => { s.prd = prd; }));
128
160
  // ---- publish: upload the map to the hosted viewer for a shareable link ----
129
- server.registerTool("publish", {
130
- title: "Publish to hosted viewer",
131
- description: "Upload this project's product map (.alkahest/map.json) to the hosted viewer (alkahest.app) and return a " +
132
- "shareable link anyone can open no install, no login to view. Only map.json is uploaded; source code never " +
133
- "leaves the machine. Run 'scan' first if the map is missing. Auth uses an API token from the ALKAHEST_TOKEN " +
134
- "env var (set it in this server's MCP config) or a prior 'alkahest login'.",
135
- inputSchema: {
136
- path: z.string().optional().describe("Project root (default: cwd)"),
137
- name: z.string().optional().describe("Project name for the link (first publish only; defaults to folder name)"),
138
- slug: z.string().optional().describe("Update an existing project by slug (else resolved from the checkout/creds)"),
139
- map: z.string().optional().describe("Which code map to publish to (a project can hold several; omit when there's one). Passing a new slug creates that code map. List them with the maps tool."),
140
- },
141
- }, async ({ path, name, slug, map }) => {
142
- const res = await publishMap(rootOf(path), { name, slug, mapSlug: map, source: "mcp" });
143
- if (!res.ok) {
144
- const hints = {
145
- no_map: "Run the scan tool first to build .alkahest/map.json.",
146
- no_token: "Set ALKAHEST_TOKEN in this MCP server's config (get a token at alkahest.app → Account).",
147
- no_api: "Set ALKAHEST_API_URL in this MCP server's config.",
148
- plan_limit: "Free plan project limit reached upgrade to Pro for more.",
149
- invalid_token: "The API token is invalid or revoked create a new one at alkahest.app → Account.",
150
- client_too_old: "This alkahest is too old to publish run 'alkahest update'.",
151
- no_workspace: "This account has no workspace yet, and a first publish needs one to hold the new project. Ask the user to create one at alkahest.app/home, then call publish again there is nothing to retry until they do.",
152
- ambiguous_map: "List the project's code maps with the maps tool, then call publish again with `map` set to one of them (or a new slug to create one).",
153
- ambiguous_project: "This checkout has no linked project and an existing one looks like it — do NOT create a duplicate. Pick a candidate's slug below (or use list_projects) and call publish again with `slug` set, or pass a deliberately new `name` to create a fresh project.",
154
- };
155
- const hint = hints[res.code ?? ""] ? ` ${hints[res.code ?? ""]}` : "";
156
- // Carry the structured map list (the edge function returns it) so the agent can pick without re-listing.
157
- const maps = res.maps?.length ? ` Maps: ${JSON.stringify(res.maps)}` : "";
158
- // Carry slug-less-publish candidates so the agent can re-publish with the right slug.
159
- const cands = res.candidates?.length
160
- ? ` Candidates: ${JSON.stringify(res.candidates.map((c) => ({ slug: c.slug, name: c.projectName, workspace: c.workspace, map: c.mapSlug })))}`
161
- : "";
162
- return text(`Publish failed (${res.code}): ${res.message}.${hint}${maps}${cands}`);
163
- }
164
- const v = await cachedUpdateStatus().catch(() => null);
165
- // Needs tail (cloud ADR-032): the server counts what's waiting on the token's user in this
166
- // project (unresolved decision questions + assigned issues). Surface it with an explicit
167
- // relay hint so agent-driven users hear about blocked decisions in their terminal.
168
- const waiting = (res.needs?.decisions ?? 0) + (res.needs?.assigned ?? 0);
169
- return json({
170
- ok: true,
171
- slug: res.slug,
172
- url: res.viewerUrl ?? res.mapUrl,
173
- created: res.created,
174
- ...(res.needs && waiting > 0
175
- ? {
176
- needs: res.needs,
177
- needsHint: `Tell the user: ${[
178
- res.needs.decisions > 0 ? `${res.needs.decisions} decision${res.needs.decisions === 1 ? "" : "s"}` : null,
179
- res.needs.assigned > 0 ? `${res.needs.assigned} assigned issue${res.needs.assigned === 1 ? "" : "s"}` : null,
180
- ].filter(Boolean).join(" and ")} waiting on them${res.needs.url ? ` at ${res.needs.url}` : ""}.`,
181
- }
182
- : {}),
183
- ...(v?.behind
184
- ? { updateAvailable: `${v.current} → ${v.latest}`, updateHint: "Tell the user to run: alkahest update" }
185
- : {}),
161
+ if (!remote)
162
+ server.registerTool("publish", {
163
+ title: "Publish to hosted viewer",
164
+ description: "Upload this project's product map (.alkahest/map.json) to the hosted viewer (alkahest.app) and return a " +
165
+ "shareable link anyone can open no install, no login to view. Only map.json is uploaded; source code never " +
166
+ "leaves the machine. Run 'scan' first if the map is missing. Auth uses an API token from the ALKAHEST_TOKEN " +
167
+ "env var (set it in this server's MCP config) or a prior 'alkahest login'.",
168
+ inputSchema: {
169
+ path: z.string().optional().describe("Project root (default: cwd)"),
170
+ name: z.string().optional().describe("Project name for the link (first publish only; defaults to folder name)"),
171
+ slug: z.string().optional().describe("Update an existing project by slug (else resolved from the checkout/creds)"),
172
+ map: z.string().optional().describe("Which code map to publish to (a project can hold several; omit when there's one). Passing a new slug creates that code map. List them with the maps tool."),
173
+ },
174
+ }, async ({ path, name, slug, map }) => {
175
+ const res = await publishMap(rootOf(path), { name, slug, mapSlug: map, source: "mcp" });
176
+ if (!res.ok) {
177
+ const hints = {
178
+ no_map: "Run the scan tool first to build .alkahest/map.json.",
179
+ no_token: "Set ALKAHEST_TOKEN in this MCP server's config (get a token at alkahest.app → Account).",
180
+ no_api: "Set ALKAHEST_API_URL in this MCP server's config.",
181
+ plan_limit: "Free plan project limit reachedupgrade to Pro for more.",
182
+ invalid_token: "The API token is invalid or revokedcreate a new one at alkahest.app → Account.",
183
+ client_too_old: "This alkahest is too old to publish — run 'alkahest update'.",
184
+ no_workspace: "This account has no workspace yet, and a first publish needs one to hold the new project. Ask the user to create one at alkahest.app/home, then call publish again — there is nothing to retry until they do.",
185
+ ambiguous_map: "List the project's code maps with the maps tool, then call publish again with `map` set to one of them (or a new slug to create one).",
186
+ ambiguous_project: "This checkout has no linked project and an existing one looks like it — do NOT create a duplicate. Pick a candidate's slug below (or use list_projects) and call publish again with `slug` set, or pass a deliberately new `name` to create a fresh project.",
187
+ };
188
+ const hint = hints[res.code ?? ""] ? ` ${hints[res.code ?? ""]}` : "";
189
+ // Carry the structured map list (the edge function returns it) so the agent can pick without re-listing.
190
+ const maps = res.maps?.length ? ` Maps: ${JSON.stringify(res.maps)}` : "";
191
+ // Carry slug-less-publish candidates so the agent can re-publish with the right slug.
192
+ const cands = res.candidates?.length
193
+ ? ` Candidates: ${JSON.stringify(res.candidates.map((c) => ({ slug: c.slug, name: c.projectName, workspace: c.workspace, map: c.mapSlug })))}`
194
+ : "";
195
+ return text(`Publish failed (${res.code}): ${res.message}.${hint}${maps}${cands}`);
196
+ }
197
+ const v = await cachedUpdateStatus().catch(() => null);
198
+ // Needs tail (cloud ADR-032): the server counts what's waiting on the token's user in this
199
+ // project (unresolved decision questions + assigned issues). Surface it with an explicit
200
+ // relay hint so agent-driven users hear about blocked decisions in their terminal.
201
+ const waiting = (res.needs?.decisions ?? 0) + (res.needs?.assigned ?? 0);
202
+ return json({
203
+ ok: true,
204
+ slug: res.slug,
205
+ url: res.viewerUrl ?? res.mapUrl,
206
+ created: res.created,
207
+ ...(res.needs && waiting > 0
208
+ ? {
209
+ needs: res.needs,
210
+ needsHint: `Tell the user: ${[
211
+ res.needs.decisions > 0 ? `${res.needs.decisions} decision${res.needs.decisions === 1 ? "" : "s"}` : null,
212
+ res.needs.assigned > 0 ? `${res.needs.assigned} assigned issue${res.needs.assigned === 1 ? "" : "s"}` : null,
213
+ ].filter(Boolean).join(" and ")} waiting on them${res.needs.url ? ` at ${res.needs.url}` : ""}.`,
214
+ }
215
+ : {}),
216
+ ...(v?.behind
217
+ ? { updateAvailable: `${v.current} → ${v.latest}`, updateHint: "Tell the user to run: alkahest update" }
218
+ : {}),
219
+ });
186
220
  });
187
- });
188
221
  // ---- check_version: let the agent tell the user whether to update ----
189
- server.registerTool("check_version", {
190
- title: "Check for alkahest updates",
191
- description: "Report the installed alkahest version vs the latest published on npm, so you can tell the user whether their " +
192
- "alkahest is current. If behind, tell them to run 'alkahest update' you can't update through MCP (the CLI " +
193
- "updates itself and this MCP server must be restarted to pick it up). No project access; just a version check.",
194
- inputSchema: {},
195
- }, async () => {
196
- const s = await checkForUpdate();
197
- return json({
198
- current: s.current,
199
- latest: s.latest,
200
- behind: s.behind,
201
- action: s.behind
202
- ? "Out of date — tell the user to run: alkahest update (then restart this MCP server)."
203
- : s.latest
204
- ? "Up to date."
205
- : s.reachable
206
- ? "Nothing published on npm to compare against."
207
- : "Couldn't reach the npm registry (offline/proxy?) — version status unknown.",
222
+ if (!remote)
223
+ server.registerTool("check_version", {
224
+ title: "Check for alkahest updates",
225
+ description: "Report the installed alkahest version vs the latest published on npm, so you can tell the user whether their " +
226
+ "alkahest is current. If behind, tell them to run 'alkahest update' you can't update through MCP (the CLI " +
227
+ "updates itself and this MCP server must be restarted to pick it up). No project access; just a version check.",
228
+ inputSchema: {},
229
+ }, async () => {
230
+ const s = await checkForUpdate();
231
+ return json({
232
+ current: s.current,
233
+ latest: s.latest,
234
+ behind: s.behind,
235
+ action: s.behind
236
+ ? "Out of date — tell the user to run: alkahest update (then restart this MCP server)."
237
+ : s.latest
238
+ ? "Up to date."
239
+ : s.reachable
240
+ ? "Nothing published on npm to compare against."
241
+ : "Couldn't reach the npm registry (offline/proxy?) — version status unknown.",
242
+ });
208
243
  });
209
- });
210
244
  // ---- comments: read map comments and act on them in-editor ----
211
245
  server.registerTool("comments", {
212
246
  title: "Map comments",
@@ -222,19 +256,19 @@ export function buildServer() {
222
256
  },
223
257
  }, async ({ path, open, project }) => {
224
258
  const root = rootOf(path);
225
- const res = await pullComments(root, { open, slug: project });
259
+ const res = await pullComments(root, withAuth({ open, slug: project }));
226
260
  if (!res.ok) {
227
- const hints = {
261
+ const hints = hinted({
228
262
  no_token: "Set ALKAHEST_TOKEN in this MCP server's config (token from alkahest.app → Account).",
229
263
  no_api: "Set ALKAHEST_API_URL in this MCP server's config.",
230
264
  no_slug: "Pass `project` — a slug from list_projects — or set ALKAHEST_PROJECT in this MCP server's config.",
231
265
  invalid_token: "The API token is invalid or revoked — create a new one at alkahest.app → Account.",
232
266
  not_found: "No accessible project for this slug.",
233
- };
267
+ });
234
268
  const hint = hints[res.code ?? ""] ? ` ${hints[res.code ?? ""]}` : "";
235
269
  return text(`Couldn't read comments (${res.code}): ${res.message}.${hint}`);
236
270
  }
237
- const map = loadMap(res.root ?? root);
271
+ const map = remote ? await getMap(undefined, res.slug) : loadMap(res.root ?? root);
238
272
  const comments = map ? enrichComments(res.comments ?? [], map) : (res.comments ?? []);
239
273
  return json({ ok: true, slug: res.slug, count: comments.length, comments });
240
274
  });
@@ -248,13 +282,13 @@ export function buildServer() {
248
282
  path: z.string().optional().describe("Project root (default: cwd)"),
249
283
  },
250
284
  }, async ({ id, resolved, path }) => {
251
- const res = await resolveComment(rootOf(path), id, resolved === undefined ? true : resolved);
285
+ const res = await resolveComment(rootOf(path), id, resolved === undefined ? true : resolved, withAuth({}));
252
286
  if (!res.ok) {
253
- const hints = {
287
+ const hints = hinted({
254
288
  no_token: "Set ALKAHEST_TOKEN in this MCP server's config.",
255
289
  forbidden: "Only the comment author or project owner can resolve it.",
256
290
  not_found: "No comment with that id.",
257
- };
291
+ });
258
292
  const hint = hints[res.code ?? ""] ? ` ${hints[res.code ?? ""]}` : "";
259
293
  return text(`Resolve failed (${res.code}): ${res.message}.${hint}`);
260
294
  }
@@ -273,19 +307,19 @@ export function buildServer() {
273
307
  },
274
308
  }, async ({ node, body, path, project }) => {
275
309
  const root = findProjectRoot(rootOf(path));
276
- const map = loadOrScan(root);
310
+ const map = remote ? await getMap(undefined, project) : loadOrScan(root);
277
311
  if (!map)
278
- return text("No map for this project — run the scan/publish tools first.");
312
+ return text(remote ? noMapMsg : "No map for this project — run the scan/publish tools first.");
279
313
  const n = resolveNode(map, node);
280
314
  if (!n)
281
315
  return text(`No node matches '${node}'. Use the overview tool to list screens/resources.`);
282
- const res = await postComment(root, { node_key: n.node_key, anchor_kind: n.anchor_kind, anchor_label: n.anchor_label, body, slug: project });
316
+ const res = await postComment(root, withAuth({ node_key: n.node_key, anchor_kind: n.anchor_kind, anchor_label: n.anchor_label, body, slug: project }));
283
317
  if (!res.ok) {
284
- const hints = {
318
+ const hints = hinted({
285
319
  no_token: "Set ALKAHEST_TOKEN in this MCP server's config.",
286
320
  no_slug: "Pass `project` — a slug from list_projects — or set ALKAHEST_PROJECT in this MCP server's config.",
287
321
  forbidden: "Only the project owner or a collaborator can comment.",
288
- };
322
+ });
289
323
  return text(`Add comment failed (${res.code}): ${res.message}.${hints[res.code ?? ""] ? " " + hints[res.code ?? ""] : ""}`);
290
324
  }
291
325
  return json({ ok: true, id: res.comment?.id, node_key: n.node_key, anchor_label: n.anchor_label });
@@ -300,49 +334,50 @@ export function buildServer() {
300
334
  path: z.string().optional().describe("Project root (default: cwd)"),
301
335
  },
302
336
  }, async ({ id, body, path }) => {
303
- const res = await postComment(rootOf(path), { parent_id: id, body });
337
+ const res = await postComment(rootOf(path), withAuth({ parent_id: id, body }));
304
338
  if (!res.ok) {
305
- const hints = {
339
+ const hints = hinted({
306
340
  no_token: "Set ALKAHEST_TOKEN in this MCP server's config.",
307
341
  not_found: "No comment with that id (parent).",
308
342
  forbidden: "Only the project owner or a collaborator can comment.",
309
- };
343
+ });
310
344
  return text(`Reply failed (${res.code}): ${res.message}.${hints[res.code ?? ""] ? " " + hints[res.code ?? ""] : ""}`);
311
345
  }
312
346
  return json({ ok: true, id: res.comment?.id, parent_id: id });
313
347
  });
314
- server.registerTool("comment_to_issue", {
315
- title: "File map comments as a GitHub issue",
316
- description: "Group one or more map comments (ids from the comments tool) into a SINGLE GitHub issue and link it back onto each. " +
317
- "Creates the issue with the local `gh` CLI (must be installed and authenticated; it runs in the project's git repo), " +
318
- "then records the issue URL on the comments so the hosted viewer shows a 'tracked' badge. Use this to turn feedback " +
319
- "into tracked work. Needs an API token; owner or collaborator only. Pass force:true to re-file comments that are " +
320
- "already linked to an issue (creates a new one).",
321
- inputSchema: {
322
- ids: z.array(z.string()).min(1).describe("Comment ids to group into one issue (from the comments tool)"),
323
- project: z.string().optional().describe("Which project (slug) say it explicitly when the folder isn't a linked checkout. List them with list_projects."),
324
- path: z.string().optional().describe("Project root (default: cwd)"),
325
- title: z.string().optional().describe("Issue title (else derived from the comments)"),
326
- repo: z.string().optional().describe("Target GitHub repo owner/repo (else gh's default for the repo)"),
327
- force: z.boolean().optional().describe("File even if some selected comments are already tracked"),
328
- },
329
- }, async ({ ids, path, title, repo, force, project }) => {
330
- const res = await fileCommentsIssue(rootOf(path), ids, { title, repo, force, slug: project });
331
- if (!res.ok) {
332
- const hints = {
333
- no_token: "Set ALKAHEST_TOKEN in this MCP server's config.",
334
- no_slug: "Pass `project` — a slug from list_projects — or set ALKAHEST_PROJECT in this MCP server's config.",
335
- already_tracked: "Some comments already have an issuepass force:true to file a new one.",
336
- gh_failed: "Install and authenticate the GitHub CLI (`gh auth login`) for this repo.",
337
- forbidden: "Only the project owner or a collaborator can file issues.",
338
- not_found: "One or more ids don't exist list them with the comments tool.",
339
- };
340
- return text(`File issue failed (${res.code}): ${res.message}.${hints[res.code ?? ""] ? " " + hints[res.code ?? ""] : ""}`);
341
- }
342
- return json({ ok: true, issue_url: res.issue_url, ids: res.ids, title: res.title });
343
- });
348
+ if (!remote)
349
+ server.registerTool("comment_to_issue", {
350
+ title: "File map comments as a GitHub issue",
351
+ description: "Group one or more map comments (ids from the comments tool) into a SINGLE GitHub issue and link it back onto each. " +
352
+ "Creates the issue with the local `gh` CLI (must be installed and authenticated; it runs in the project's git repo), " +
353
+ "then records the issue URL on the comments so the hosted viewer shows a 'tracked' badge. Use this to turn feedback " +
354
+ "into tracked work. Needs an API token; owner or collaborator only. Pass force:true to re-file comments that are " +
355
+ "already linked to an issue (creates a new one).",
356
+ inputSchema: {
357
+ ids: z.array(z.string()).min(1).describe("Comment ids to group into one issue (from the comments tool)"),
358
+ project: z.string().optional().describe("Which project (slug) — say it explicitly when the folder isn't a linked checkout. List them with list_projects."),
359
+ path: z.string().optional().describe("Project root (default: cwd)"),
360
+ title: z.string().optional().describe("Issue title (else derived from the comments)"),
361
+ repo: z.string().optional().describe("Target GitHub repo owner/repo (else gh's default for the repo)"),
362
+ force: z.boolean().optional().describe("File even if some selected comments are already tracked"),
363
+ },
364
+ }, async ({ ids, path, title, repo, force, project }) => {
365
+ const res = await fileCommentsIssue(rootOf(path), ids, { title, repo, force, slug: project });
366
+ if (!res.ok) {
367
+ const hints = {
368
+ no_token: "Set ALKAHEST_TOKEN in this MCP server's config.",
369
+ no_slug: "Pass `project` a slug from list_projects or set ALKAHEST_PROJECT in this MCP server's config.",
370
+ already_tracked: "Some comments already have an issue pass force:true to file a new one.",
371
+ gh_failed: "Install and authenticate the GitHub CLI (`gh auth login`) for this repo.",
372
+ forbidden: "Only the project owner or a collaborator can file issues.",
373
+ not_found: "One or more ids don't exist — list them with the comments tool.",
374
+ };
375
+ return text(`File issue failed (${res.code}): ${res.message}.${hints[res.code ?? ""] ? " " + hints[res.code ?? ""] : ""}`);
376
+ }
377
+ return json({ ok: true, issue_url: res.issue_url, ids: res.ids, title: res.title });
378
+ });
344
379
  // ---- issues: the Issue Map — a map-shaped issue tracker on the hosted viewer ----
345
- const issueHints = {
380
+ const issueHints = hinted({
346
381
  no_token: "Set ALKAHEST_TOKEN in this MCP server's config (token from alkahest.app → Account).",
347
382
  no_api: "Set ALKAHEST_API_URL in this MCP server's config.",
348
383
  no_slug: "Pass `project` — a slug from list_projects — or set ALKAHEST_PROJECT in this MCP server's config.",
@@ -350,7 +385,7 @@ export function buildServer() {
350
385
  forbidden: "Only the project owner or a collaborator can write issues.",
351
386
  not_found: "Not found — list ids with the issues tool, or the project's issue maps with the maps tool.",
352
387
  ambiguous_map: "List the project's issue maps with the maps tool, then retry with `map` set to one (or create one with create_map).",
353
- };
388
+ });
354
389
  // `maps` (present on ambiguous_map / unknown-slug) is appended as JSON so the agent can pick a map
355
390
  // without a second round-trip to the maps tool.
356
391
  const issueFail = (what, code, message, maps) => text(`${what} failed (${code}): ${message}.${issueHints[code ?? ""] ? " " + issueHints[code ?? ""] : ""}${maps?.length ? ` Maps: ${JSON.stringify(maps)}` : ""}`);
@@ -372,7 +407,7 @@ export function buildServer() {
372
407
  q: z.string().optional().describe("Filter issues by title/body substring (server-side). Edges/links stay unfiltered, so blockedBy may name issues outside the filtered list."),
373
408
  },
374
409
  }, async ({ path, open, map, q, project }) => {
375
- const res = await pullIssues(rootOf(path), { mapSlug: map, q, slug: project });
410
+ const res = await pullIssues(rootOf(path), withAuth({ mapSlug: map, q, slug: project }));
376
411
  if (!res.ok || !res.graph)
377
412
  return issueFail("Read issues", res.code, res.message, res.maps);
378
413
  const states = deriveIssueStates(res.graph);
@@ -422,7 +457,7 @@ export function buildServer() {
422
457
  target_key: target,
423
458
  }
424
459
  : {};
425
- const res = await createIssue(rootOf(path), { title, type, status, body, priority, due_on, assignee_id, props, parent_id, mapSlug: map, slug: project, ...targetFields });
460
+ const res = await createIssue(rootOf(path), withAuth({ title, type, status, body, priority, due_on, assignee_id, props, parent_id, mapSlug: map, slug: project, ...targetFields }));
426
461
  if (!res.ok || !res.issue)
427
462
  return issueFail("Add issue", res.code, res.message, res.maps);
428
463
  return json({ ok: true, issue: res.issue });
@@ -463,7 +498,7 @@ export function buildServer() {
463
498
  path: z.string().optional().describe("Project root (default: cwd — used only to find your token/API)"),
464
499
  },
465
500
  }, async ({ status, project, q, path }) => {
466
- const res = await pullTasks(rootOf(path), { status, project, q });
501
+ const res = await pullTasks(rootOf(path), withAuth({ status, project, q }));
467
502
  if (!res.ok || !res.tasks)
468
503
  return issueFail("List tasks", res.code, res.message);
469
504
  return json({ ok: true, count: res.tasks.length, tasks: res.tasks });
@@ -510,7 +545,7 @@ export function buildServer() {
510
545
  path: z.string().optional().describe("Project root (default: cwd — a linked checkout auto-tags its project)"),
511
546
  },
512
547
  }, async ({ title, body, project, workspace, tags, due_on, dedup_key, note_mode, note, skill, path }) => {
513
- const res = await createTask(rootOf(path), { title, body, slug: project, workspace, tags, due_on, dedup_key, note_mode, note, skill });
548
+ const res = await createTask(rootOf(path), withAuth({ title, body, slug: project, workspace, tags, due_on, dedup_key, note_mode, note, skill }));
514
549
  if (!res.ok || !res.task) {
515
550
  const wsHint = res.workspaces?.length ? ` Workspaces: ${JSON.stringify(res.workspaces)}` : "";
516
551
  return issueFail("Add task", res.code, `${res.message ?? ""}${wsHint}`);
@@ -531,7 +566,7 @@ export function buildServer() {
531
566
  path: z.string().optional().describe("Project root (default: cwd — used only to find your token/API)"),
532
567
  },
533
568
  }, async ({ id, reopen, path }) => {
534
- const res = await completeTask(rootOf(path), { id, reopen });
569
+ const res = await completeTask(rootOf(path), withAuth({ id, reopen }));
535
570
  if (!res.ok || !res.task)
536
571
  return issueFail(reopen ? "Reopen task" : "Complete task", res.code, res.message);
537
572
  return json({ ok: true, task: res.task });
@@ -556,7 +591,7 @@ export function buildServer() {
556
591
  path: z.string().optional().describe("Project root (default: cwd — used only to find your token/API)"),
557
592
  },
558
593
  }, async ({ id, title, body, due_on, tags, note_mode, note, skill, path }) => {
559
- const res = await updateTask(rootOf(path), {
594
+ const res = await updateTask(rootOf(path), withAuth({
560
595
  id,
561
596
  title,
562
597
  body: body === "" ? null : body,
@@ -565,7 +600,7 @@ export function buildServer() {
565
600
  note_mode: note_mode === "" ? null : note_mode,
566
601
  note: note === "" ? null : note,
567
602
  skill: skill === "" ? null : skill,
568
- });
603
+ }));
569
604
  if (!res.ok || !res.task)
570
605
  return issueFail("Update task", res.code, res.message);
571
606
  return json({ ok: true, task: res.task });
@@ -586,7 +621,7 @@ export function buildServer() {
586
621
  path: z.string().optional().describe("Project root (default: cwd — used only to find your token/API)"),
587
622
  },
588
623
  }, async ({ path }) => {
589
- const res = await pullSkills(rootOf(path));
624
+ const res = await pullSkills(rootOf(path), withAuth({}));
590
625
  if (!res.ok || !res.skills)
591
626
  return issueFail("List skills", res.code, res.message);
592
627
  return json({ ok: true, count: res.skills.length, skills: res.skills });
@@ -613,7 +648,7 @@ export function buildServer() {
613
648
  path: z.string().optional().describe("Project root (default: cwd — used only to find your token/API)"),
614
649
  },
615
650
  }, async ({ name, body, default_for, rename_from, path }) => {
616
- const res = await saveSkill(rootOf(path), { name, body, default_for, rename_from });
651
+ const res = await saveSkill(rootOf(path), withAuth({ name, body, default_for, rename_from }));
617
652
  if (!res.ok || !res.skill)
618
653
  return issueFail("Add skill", res.code, res.message);
619
654
  return json({ ok: true, skill: res.skill });
@@ -634,7 +669,7 @@ export function buildServer() {
634
669
  path: z.string().optional().describe("Project root (default: cwd — used only to find your token/API)"),
635
670
  },
636
671
  }, async ({ task, open, path }) => {
637
- const res = await pullTaskComments(rootOf(path), { task, open });
672
+ const res = await pullTaskComments(rootOf(path), withAuth({ task, open }));
638
673
  if (!res.ok || !res.comments)
639
674
  return issueFail("Read task comments", res.code, res.message);
640
675
  return json({ ok: true, count: res.comments.length, comments: res.comments });
@@ -652,7 +687,7 @@ export function buildServer() {
652
687
  path: z.string().optional().describe("Project root (default: cwd — used only to find your token/API)"),
653
688
  },
654
689
  }, async ({ task, body, path }) => {
655
- const res = await postTaskComment(rootOf(path), { task_id: task, body, kind: "question" });
690
+ const res = await postTaskComment(rootOf(path), withAuth({ task_id: task, body, kind: "question" }));
656
691
  if (!res.ok || !res.comment)
657
692
  return issueFail("Ask task", res.code, res.message);
658
693
  return json({ ok: true, comment: res.comment, note: "Question posted — re-check with task_comments, then resolve_task_comment once answered." });
@@ -673,7 +708,7 @@ export function buildServer() {
673
708
  path: z.string().optional().describe("Project root (default: cwd — used only to find your token/API)"),
674
709
  },
675
710
  }, async ({ task, parent, body, kind, path }) => {
676
- const res = await postTaskComment(rootOf(path), { task_id: task, parent, body, kind });
711
+ const res = await postTaskComment(rootOf(path), withAuth({ task_id: task, parent, body, kind }));
677
712
  if (!res.ok || !res.comment)
678
713
  return issueFail("Comment on task", res.code, res.message);
679
714
  return json({ ok: true, comment: res.comment });
@@ -690,7 +725,7 @@ export function buildServer() {
690
725
  path: z.string().optional().describe("Project root (default: cwd — used only to find your token/API)"),
691
726
  },
692
727
  }, async ({ id, resolved, path }) => {
693
- const res = await resolveTaskComment(rootOf(path), { id, resolved });
728
+ const res = await resolveTaskComment(rootOf(path), withAuth({ id, resolved }));
694
729
  if (!res.ok)
695
730
  return issueFail("Resolve task comment", res.code, res.message);
696
731
  return json({ ok: true, id: res.id, resolved: res.resolved });
@@ -713,9 +748,9 @@ export function buildServer() {
713
748
  }, async ({ q, path, project }) => {
714
749
  const root = rootOf(path);
715
750
  const [notesRes, issuesRes, tasksRes] = await Promise.all([
716
- pullNotes(root, { q, bodies: "excerpt", slug: project }),
717
- pullIssues(root, { q, slug: project }),
718
- pullTasks(root, { q, status: "all", project }),
751
+ pullNotes(root, withAuth({ q, bodies: "excerpt", slug: project })),
752
+ pullIssues(root, withAuth({ q, slug: project })),
753
+ pullTasks(root, withAuth({ q, status: "all", project })),
719
754
  ]);
720
755
  const notes = notesRes.ok && notesRes.maps
721
756
  ? notesRes.maps.flatMap((m) => m.notes.map((n) => ({ slug: n.slug, title: n.title, map: m.slug, folder: n.folder ?? null, excerpt: n.body ?? null })))
@@ -750,7 +785,7 @@ export function buildServer() {
750
785
  full_bodies: z.boolean().optional().describe("Return complete bodies instead of 240-char excerpts (heavy on a big wiki)"),
751
786
  },
752
787
  }, async ({ path, q, map, full_bodies, project }) => {
753
- const res = await pullNotes(rootOf(path), { mapSlug: map, q, bodies: full_bodies ? undefined : "excerpt", slug: project });
788
+ const res = await pullNotes(rootOf(path), withAuth({ mapSlug: map, q, bodies: full_bodies ? undefined : "excerpt", slug: project }));
754
789
  if (!res.ok || !res.maps)
755
790
  return issueFail("Read notes", res.code, res.message, res.mapList);
756
791
  return json({ ok: true, project: res.project, count: res.maps.reduce((n, m) => n + m.notes.length, 0), maps: res.maps });
@@ -766,7 +801,7 @@ export function buildServer() {
766
801
  path: z.string().optional().describe("Project root (default: cwd)"),
767
802
  },
768
803
  }, async ({ note, map, path, project }) => {
769
- const res = await getNote(rootOf(path), { note, mapSlug: map, slug: project });
804
+ const res = await getNote(rootOf(path), withAuth({ note, mapSlug: map, slug: project }));
770
805
  if (!res.ok || !res.note)
771
806
  return issueFail("Get note", res.code, res.message, res.mapList);
772
807
  const { ok: _ok, code: _code, message: _message, mapList: _ml, ...rest } = res;
@@ -790,7 +825,7 @@ export function buildServer() {
790
825
  path: z.string().optional().describe("Project root (default: cwd)"),
791
826
  },
792
827
  }, async ({ title, body, note_slug, folder, props, map, path, project }) => {
793
- const res = await createNote(rootOf(path), { title, body, note_slug, folder, props, mapSlug: map, slug: project });
828
+ const res = await createNote(rootOf(path), withAuth({ title, body, note_slug, folder, props, mapSlug: map, slug: project }));
794
829
  if (!res.ok || !res.note)
795
830
  return issueFail("Add note", res.code, res.message, res.maps);
796
831
  return json({ ok: true, note: res.note });
@@ -811,7 +846,7 @@ export function buildServer() {
811
846
  path: z.string().optional().describe("Project root (default: cwd)"),
812
847
  },
813
848
  }, async ({ from, to, remove, path, project }) => {
814
- const res = await linkNotes(rootOf(path), { from, to, remove, slug: project });
849
+ const res = await linkNotes(rootOf(path), withAuth({ from, to, remove, slug: project }));
815
850
  if (!res.ok)
816
851
  return issueFail(remove ? "Unlink notes" : "Link notes", res.code, res.message, res.maps);
817
852
  return json({ ok: true, ...(remove ? { removed: `${from} → ${to}` } : { linked: `${from} → ${to}` }) });
@@ -829,7 +864,7 @@ export function buildServer() {
829
864
  path: z.string().optional().describe("Project root (default: cwd)"),
830
865
  },
831
866
  }, async ({ note, map, path, project }) => {
832
- const res = await mapNote(rootOf(path), { noteRef: note, mapSlug: map, slug: project });
867
+ const res = await mapNote(rootOf(path), withAuth({ noteRef: note, mapSlug: map, slug: project }));
833
868
  if (!res.ok)
834
869
  return issueFail("Move note", res.code, res.message, res.maps);
835
870
  return json({ ok: true, note: res.note, map: res.map });
@@ -859,7 +894,7 @@ export function buildServer() {
859
894
  path: z.string().optional().describe("Project root (default: cwd)"),
860
895
  },
861
896
  }, async ({ note, title, body, new_slug, folder, props, delete: del, reason, restore, map, path, project }) => {
862
- const res = await updateNote(rootOf(path), { note, title, body, new_slug, folder, props, delete: del, reason, restore, mapSlug: map, slug: project });
897
+ const res = await updateNote(rootOf(path), withAuth({ note, title, body, new_slug, folder, props, delete: del, reason, restore, mapSlug: map, slug: project }));
863
898
  const what = del ? "Delete note" : restore ? "Restore note" : "Update note";
864
899
  if (!res.ok)
865
900
  return issueFail(what, res.code, res.message, res.maps);
@@ -899,7 +934,7 @@ export function buildServer() {
899
934
  if (!define?.length && !remove?.length) {
900
935
  return json({ ok: false, error: "bad_request", message: "Pass `define` (definitions to register) and/or `remove` (keys to unregister)." });
901
936
  }
902
- const res = await editPropDefs(rootOf(path), { defs: define, remove, mapSlug: map, slug: project });
937
+ const res = await editPropDefs(rootOf(path), withAuth({ defs: define, remove, mapSlug: map, slug: project }));
903
938
  if (!res.ok)
904
939
  return issueFail("Edit note props", res.code, res.message, res.maps);
905
940
  return json({ ok: true, added: res.added ?? 0, merged: res.merged ?? 0, removed: res.removed ?? 0, skipped: res.skipped ?? 0 });
@@ -916,7 +951,7 @@ export function buildServer() {
916
951
  type: z.enum(["code", "issue", "note"]).optional().describe("Restrict to one type (default: all)"),
917
952
  },
918
953
  }, async ({ path, type, project }) => {
919
- const res = await listMaps(rootOf(path), { type, slug: project });
954
+ const res = await listMaps(rootOf(path), withAuth({ type, slug: project }));
920
955
  if (!res.ok || !res.maps)
921
956
  return issueFail("List maps", res.code, res.message);
922
957
  return json({ ok: true, slug: res.slug, count: res.maps.length, maps: res.maps });
@@ -932,10 +967,10 @@ export function buildServer() {
932
967
  "(screens/resources counts) so you can match a local scan by structure. Needs an API token; no project context.",
933
968
  inputSchema: {},
934
969
  }, async () => {
935
- const res = await listProjects({});
970
+ const res = await listProjects(withAuth({}));
936
971
  if (!res.ok || !res.projects) {
937
- const hint = res.code === "no_token" ? " Set ALKAHEST_TOKEN in this MCP server's config." : "";
938
- return text(`List projects failed (${res.code}): ${res.message}.${hint}`);
972
+ const h = hinted({ no_token: "Set ALKAHEST_TOKEN in this MCP server's config." })[res.code ?? ""];
973
+ return text(`List projects failed (${res.code}): ${res.message}.${h ? ` ${h}` : ""}`);
939
974
  }
940
975
  return json({
941
976
  ok: true,
@@ -966,7 +1001,7 @@ export function buildServer() {
966
1001
  limit: z.number().optional().describe("Max versions (default 50)"),
967
1002
  },
968
1003
  }, async ({ path, map, limit, project }) => {
969
- const res = await listHistory(rootOf(path), { map, limit, slug: project });
1004
+ const res = await listHistory(rootOf(path), withAuth({ map, limit, slug: project }));
970
1005
  if (!res.ok || !res.versions)
971
1006
  return issueFail("History", res.code, res.message);
972
1007
  // Newest first; include count deltas vs the previous version so the agent needn't recompute.
@@ -1000,7 +1035,7 @@ export function buildServer() {
1000
1035
  path: z.string().optional().describe("Project root (default: cwd)"),
1001
1036
  },
1002
1037
  }, async ({ slug, type, name, path, project }) => {
1003
- const res = await createMap(rootOf(path), { mapSlug: slug, type, mapName: name, slug: project });
1038
+ const res = await createMap(rootOf(path), withAuth({ mapSlug: slug, type, mapName: name, slug: project }));
1004
1039
  if (!res.ok || !res.map)
1005
1040
  return issueFail("Create map", res.code, res.message);
1006
1041
  return json({ ok: true, map: res.map });
@@ -1050,7 +1085,7 @@ export function buildServer() {
1050
1085
  set.target_key = target;
1051
1086
  }
1052
1087
  }
1053
- const res = await updateIssue(rootOf(path), { id, ...(del ? { delete: true } : { set }) });
1088
+ const res = await updateIssue(rootOf(path), withAuth({ id, ...(del ? { delete: true } : { set }) }));
1054
1089
  if (!res.ok)
1055
1090
  return issueFail("Update issue", res.code, res.message);
1056
1091
  return json(res.deleted ? { ok: true, deleted: true, id } : { ok: true, issue: res.issue });
@@ -1069,7 +1104,7 @@ export function buildServer() {
1069
1104
  },
1070
1105
  }, async ({ from, to, kind, remove, path }) => {
1071
1106
  const edge = [{ to, kind: kind ?? "blocks" }];
1072
- const res = await updateIssue(rootOf(path), { id: from, ...(remove ? { remove_edges: edge } : { add_edges: edge }) });
1107
+ const res = await updateIssue(rootOf(path), withAuth({ id: from, ...(remove ? { remove_edges: edge } : { add_edges: edge }) }));
1073
1108
  if (!res.ok)
1074
1109
  return issueFail("Link issues", res.code, res.message);
1075
1110
  return json({ ok: true, from, to, kind: kind ?? "blocks", removed: Boolean(remove) });
@@ -1088,7 +1123,7 @@ export function buildServer() {
1088
1123
  path: z.string().optional().describe("Project root (default: cwd)"),
1089
1124
  },
1090
1125
  }, async ({ issue, map, remove, path, project }) => {
1091
- const res = await mapIssue(rootOf(path), { issueId: issue, mapSlug: map, remove, slug: project });
1126
+ const res = await mapIssue(rootOf(path), withAuth({ issueId: issue, mapSlug: map, remove, slug: project }));
1092
1127
  if (!res.ok)
1093
1128
  return issueFail(remove ? "Unmap issue" : "Map issue", res.code, res.message, res.maps);
1094
1129
  return json({ ok: true, issue: res.issue, map: res.map, member: res.member });
@@ -1108,7 +1143,7 @@ export function buildServer() {
1108
1143
  path: z.string().optional().describe("Project root (default: cwd)"),
1109
1144
  },
1110
1145
  }, async ({ issue, open, path, project }) => {
1111
- const res = await pullIssueComments(rootOf(path), { issue, open, slug: project });
1146
+ const res = await pullIssueComments(rootOf(path), withAuth({ issue, open, slug: project }));
1112
1147
  if (!res.ok || !res.comments)
1113
1148
  return issueFail("Read issue comments", res.code, res.message);
1114
1149
  return json({ ok: true, count: res.comments.length, comments: res.comments });
@@ -1129,7 +1164,7 @@ export function buildServer() {
1129
1164
  path: z.string().optional().describe("Project root (default: cwd)"),
1130
1165
  },
1131
1166
  }, async ({ issue, body, mention, path }) => {
1132
- const res = await postIssueComment(rootOf(path), { issue_id: issue, body, kind: "question", mention });
1167
+ const res = await postIssueComment(rootOf(path), withAuth({ issue_id: issue, body, kind: "question", mention }));
1133
1168
  if (!res.ok || !res.comment)
1134
1169
  return issueFail("Ask issue", res.code, res.message);
1135
1170
  return json({ ok: true, comment: res.comment, note: "Question posted — the issue is now awaiting the user's decision. Re-check with issue_comments, then resolve_issue_question once answered." });
@@ -1149,7 +1184,7 @@ export function buildServer() {
1149
1184
  path: z.string().optional().describe("Project root (default: cwd)"),
1150
1185
  },
1151
1186
  }, async ({ issue, parent, body, kind, mention, path }) => {
1152
- const res = await postIssueComment(rootOf(path), { issue_id: issue, parent, body, kind, mention });
1187
+ const res = await postIssueComment(rootOf(path), withAuth({ issue_id: issue, parent, body, kind, mention }));
1153
1188
  if (!res.ok || !res.comment)
1154
1189
  return issueFail("Reply on issue", res.code, res.message);
1155
1190
  return json({ ok: true, comment: res.comment });
@@ -1165,7 +1200,7 @@ export function buildServer() {
1165
1200
  path: z.string().optional().describe("Project root (default: cwd)"),
1166
1201
  },
1167
1202
  }, async ({ id, resolved, path }) => {
1168
- const res = await resolveIssueComment(rootOf(path), { id, resolved });
1203
+ const res = await resolveIssueComment(rootOf(path), withAuth({ id, resolved }));
1169
1204
  if (!res.ok)
1170
1205
  return issueFail("Resolve question", res.code, res.message);
1171
1206
  return json({ ok: true, id: res.id, resolved: res.resolved });
@@ -1189,7 +1224,7 @@ export function buildServer() {
1189
1224
  // Resolve the terminal status to move to (explicit, else the project's first terminal status).
1190
1225
  let target = status;
1191
1226
  if (!target) {
1192
- const g = await pullIssues(root, { slug: project });
1227
+ const g = await pullIssues(root, withAuth({ slug: project }));
1193
1228
  if (!g.ok || !g.graph)
1194
1229
  return issueFail("Complete issue", g.code, g.message, g.maps);
1195
1230
  const terminal = [...terminalStatuses(g.graph.issue_config)];
@@ -1197,10 +1232,10 @@ export function buildServer() {
1197
1232
  return text("Complete issue failed: this project's issue_config has no terminal status. Set one, or use update_issue with an explicit status.");
1198
1233
  target = terminal[0];
1199
1234
  }
1200
- const upd = await updateIssue(root, { id, set: { status: target } });
1235
+ const upd = await updateIssue(root, withAuth({ id, set: { status: target } }));
1201
1236
  if (!upd.ok)
1202
1237
  return issueFail("Complete issue", upd.code, upd.message);
1203
- const cmt = await postIssueComment(root, { issue_id: id, body: result, kind: "result" });
1238
+ const cmt = await postIssueComment(root, withAuth({ issue_id: id, body: result, kind: "result" }));
1204
1239
  if (!cmt.ok)
1205
1240
  return issueFail("Complete issue (result note)", cmt.code, cmt.message);
1206
1241
  return json({