@cr8rcho/alkahest 0.1.76 → 0.1.78

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,77 @@ 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() {
25
- const server = new McpServer({ name: "alkahest", version: pkg.version });
22
+ export function buildServer(remote) {
23
+ const server = new McpServer({
24
+ name: "alkahest",
25
+ title: "Alkahest",
26
+ version: pkg.version,
27
+ // Connector-facing identity (MCP `icons`/`websiteUrl`): clients that render server metadata
28
+ // (e.g. claude.ai custom connectors) show these instead of a generic placeholder glyph.
29
+ websiteUrl: "https://www.alkahest.app",
30
+ icons: [
31
+ { src: "https://www.alkahest.app/icon.svg", mimeType: "image/svg+xml", sizes: ["any"] },
32
+ { src: "https://www.alkahest.app/apple-icon.png", mimeType: "image/png", sizes: ["180x180"] },
33
+ ],
34
+ });
26
35
  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,
36
+ /** Thread the connector's token/api into a core call's params (no-op for local stdio). */
37
+ const withAuth = (params) => remote ? { ...params, token: remote.token, api: remote.api } : params;
38
+ /** The product map: the local checkout's map.json, or (remote) the published bytes. */
39
+ const getMap = async (path, project, mapSlug) => remote ? fetchPublishedMap({ api: remote.api, token: remote.token, project, mapSlug }) : loadOrScan(rootOf(path));
40
+ const noMapMsg = remote
41
+ ? "No published code map. Pass `project` (a slug from list_projects), and `map` when the project has several code maps."
42
+ : "No screens, or unsupported project.";
43
+ /** Remote connectors have no env/config to edit — steer those hints to the connector URL + `project` arg. */
44
+ const hinted = (local) => remote
45
+ ? {
46
+ ...local,
47
+ no_token: "The connector URL is missing its token — recreate it at alkahest.app → API tokens.",
48
+ no_api: "The connector's API base is misconfigured.",
49
+ no_slug: "Pass `project` — a slug from list_projects.",
50
+ 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.",
51
+ }
52
+ : local;
53
+ if (!remote)
54
+ server.registerTool("scan", {
55
+ title: "Scan project",
56
+ description: "Statically analyze a React/Next project to create/update a product map (.alkahest/map.json). " +
57
+ "Extracts screens, transitions between screens, and the API/data calls each screen makes. Returns a result summary (counts). " +
58
+ "The map is viewed on the hosted viewer — run the publish tool for a shareable link.",
59
+ inputSchema: { path: z.string().optional().describe("Project root (default: cwd)") },
60
+ }, async ({ path }) => {
61
+ const result = runScan(rootOf(path));
62
+ if (!result)
63
+ return text("No screens found. Only Next app-router (page.* under app/ or src/app/) is supported.");
64
+ const m = result.map;
65
+ return json({
66
+ framework: m.meta.framework,
67
+ router: m.meta.router,
68
+ screens: m.screens.length,
69
+ resources: m.resources.length,
70
+ transitions: m.transitions.length,
71
+ calls: m.calls.length,
72
+ mapPath: result.outFile,
73
+ });
46
74
  });
47
- });
48
75
  server.registerTool("overview", {
49
76
  title: "Product map overview",
50
77
  description: "Full product map overview: list of screens (route/title/feature count) and list of resources (label/number of calling screens). " +
51
78
  "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));
79
+ inputSchema: {
80
+ path: z.string().optional(),
81
+ project: z.string().optional().describe("Which project (slug) — remote connectors read the PUBLISHED code map (slugs from list_projects). Ignored locally."),
82
+ map: z.string().optional().describe("Which code map when the project has several (remote connectors only; default: the oldest)"),
83
+ },
84
+ }, async ({ path, project, map: mapSlug }) => {
85
+ const map = await getMap(path, project, mapSlug);
55
86
  if (!map)
56
- return text("No screens, or unsupported project.");
87
+ return text(noMapMsg);
57
88
  return json({
58
89
  framework: map.meta.framework,
59
90
  router: map.meta.router,
@@ -76,11 +107,16 @@ export function buildServer() {
76
107
  title: "Screen detail",
77
108
  description: "Full structure of one screen: UI features, outgoing/incoming transitions, called resources (API/data), components, and source location. " +
78
109
  "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));
110
+ inputSchema: {
111
+ screen: z.string().describe("screen id / route / title"),
112
+ path: z.string().optional(),
113
+ project: z.string().optional().describe("Which project (slug) — remote connectors read the PUBLISHED code map (slugs from list_projects). Ignored locally."),
114
+ map: z.string().optional().describe("Which code map when the project has several (remote connectors only; default: the oldest)"),
115
+ },
116
+ }, async ({ screen, path, project, map: mapSlug }) => {
117
+ const map = await getMap(path, project, mapSlug);
82
118
  if (!map)
83
- return text("No screens, or unsupported project.");
119
+ return text(noMapMsg);
84
120
  const s = matchScreen(map, screen);
85
121
  if (!s)
86
122
  return text(`Screen not found: ${screen}`);
@@ -90,11 +126,16 @@ export function buildServer() {
90
126
  title: "Resource callers (impact)",
91
127
  description: "Returns the screens that call a specific resource (API endpoint/data). For understanding data dependencies and change impact. " +
92
128
  "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));
129
+ inputSchema: {
130
+ resource: z.string(),
131
+ path: z.string().optional(),
132
+ project: z.string().optional().describe("Which project (slug) — remote connectors read the PUBLISHED code map (slugs from list_projects). Ignored locally."),
133
+ map: z.string().optional().describe("Which code map when the project has several (remote connectors only; default: the oldest)"),
134
+ },
135
+ }, async ({ resource, path, project, map: mapSlug }) => {
136
+ const map = await getMap(path, project, mapSlug);
96
137
  if (!map)
97
- return text("No screens, or unsupported project.");
138
+ return text(noMapMsg);
98
139
  const q = resource.toLowerCase();
99
140
  const matched = map.resources.filter((r) => r.id.toLowerCase() === q || (r.path ?? "").toLowerCase().includes(q) || r.label.toLowerCase().includes(q));
100
141
  return json(matched.map((r) => ({
@@ -105,108 +146,112 @@ export function buildServer() {
105
146
  })));
106
147
  });
107
148
  // ---- 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; }));
149
+ if (!remote)
150
+ server.registerTool("set_summary", {
151
+ title: "Set screen summary",
152
+ description: "Save a one-line, PM-friendly summary ('what the user does here') onto a screen in map.json it appears " +
153
+ "in the screen's panel on the hosted viewer after the next publish. Write the summary yourself from get_screen data.",
154
+ inputSchema: {
155
+ screen: z.string().describe("screen id / route / title"),
156
+ summary: z.string().describe("a 1-2 sentence summary in the user's language"),
157
+ path: z.string().optional(),
158
+ },
159
+ }, async ({ screen, summary, path }) => writeField(rootOf(path), screen, (s) => { s.summary = summary; }));
160
+ if (!remote)
161
+ server.registerTool("set_prd", {
162
+ title: "Set screen PRD",
163
+ description: "Save a PRD/requirements markdown onto a screen in map.json — it appears in the screen's panel on the " +
164
+ "hosted viewer (rendered) after the next publish. Write the PRD yourself from get_screen / who_calls data.",
165
+ inputSchema: {
166
+ screen: z.string().describe("screen id / route / title"),
167
+ prd: z.string().describe("PRD/requirements as markdown"),
168
+ path: z.string().optional(),
169
+ },
170
+ }, async ({ screen, prd, path }) => writeField(rootOf(path), screen, (s) => { s.prd = prd; }));
128
171
  // ---- 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
- : {}),
172
+ if (!remote)
173
+ server.registerTool("publish", {
174
+ title: "Publish to hosted viewer",
175
+ description: "Upload this project's product map (.alkahest/map.json) to the hosted viewer (alkahest.app) and return a " +
176
+ "shareable link anyone can open no install, no login to view. Only map.json is uploaded; source code never " +
177
+ "leaves the machine. Run 'scan' first if the map is missing. Auth uses an API token from the ALKAHEST_TOKEN " +
178
+ "env var (set it in this server's MCP config) or a prior 'alkahest login'.",
179
+ inputSchema: {
180
+ path: z.string().optional().describe("Project root (default: cwd)"),
181
+ name: z.string().optional().describe("Project name for the link (first publish only; defaults to folder name)"),
182
+ slug: z.string().optional().describe("Update an existing project by slug (else resolved from the checkout/creds)"),
183
+ 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."),
184
+ },
185
+ }, async ({ path, name, slug, map }) => {
186
+ const res = await publishMap(rootOf(path), { name, slug, mapSlug: map, source: "mcp" });
187
+ if (!res.ok) {
188
+ const hints = {
189
+ no_map: "Run the scan tool first to build .alkahest/map.json.",
190
+ no_token: "Set ALKAHEST_TOKEN in this MCP server's config (get a token at alkahest.app → Account).",
191
+ no_api: "Set ALKAHEST_API_URL in this MCP server's config.",
192
+ plan_limit: "Free plan project limit reachedupgrade to Pro for more.",
193
+ invalid_token: "The API token is invalid or revokedcreate a new one at alkahest.app → Account.",
194
+ client_too_old: "This alkahest is too old to publish — run 'alkahest update'.",
195
+ 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.",
196
+ 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).",
197
+ 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.",
198
+ };
199
+ const hint = hints[res.code ?? ""] ? ` ${hints[res.code ?? ""]}` : "";
200
+ // Carry the structured map list (the edge function returns it) so the agent can pick without re-listing.
201
+ const maps = res.maps?.length ? ` Maps: ${JSON.stringify(res.maps)}` : "";
202
+ // Carry slug-less-publish candidates so the agent can re-publish with the right slug.
203
+ const cands = res.candidates?.length
204
+ ? ` Candidates: ${JSON.stringify(res.candidates.map((c) => ({ slug: c.slug, name: c.projectName, workspace: c.workspace, map: c.mapSlug })))}`
205
+ : "";
206
+ return text(`Publish failed (${res.code}): ${res.message}.${hint}${maps}${cands}`);
207
+ }
208
+ const v = await cachedUpdateStatus().catch(() => null);
209
+ // Needs tail (cloud ADR-032): the server counts what's waiting on the token's user in this
210
+ // project (unresolved decision questions + assigned issues). Surface it with an explicit
211
+ // relay hint so agent-driven users hear about blocked decisions in their terminal.
212
+ const waiting = (res.needs?.decisions ?? 0) + (res.needs?.assigned ?? 0);
213
+ return json({
214
+ ok: true,
215
+ slug: res.slug,
216
+ url: res.viewerUrl ?? res.mapUrl,
217
+ created: res.created,
218
+ ...(res.needs && waiting > 0
219
+ ? {
220
+ needs: res.needs,
221
+ needsHint: `Tell the user: ${[
222
+ res.needs.decisions > 0 ? `${res.needs.decisions} decision${res.needs.decisions === 1 ? "" : "s"}` : null,
223
+ res.needs.assigned > 0 ? `${res.needs.assigned} assigned issue${res.needs.assigned === 1 ? "" : "s"}` : null,
224
+ ].filter(Boolean).join(" and ")} waiting on them${res.needs.url ? ` at ${res.needs.url}` : ""}.`,
225
+ }
226
+ : {}),
227
+ ...(v?.behind
228
+ ? { updateAvailable: `${v.current} → ${v.latest}`, updateHint: "Tell the user to run: alkahest update" }
229
+ : {}),
230
+ });
186
231
  });
187
- });
188
232
  // ---- 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.",
233
+ if (!remote)
234
+ server.registerTool("check_version", {
235
+ title: "Check for alkahest updates",
236
+ description: "Report the installed alkahest version vs the latest published on npm, so you can tell the user whether their " +
237
+ "alkahest is current. If behind, tell them to run 'alkahest update' you can't update through MCP (the CLI " +
238
+ "updates itself and this MCP server must be restarted to pick it up). No project access; just a version check.",
239
+ inputSchema: {},
240
+ }, async () => {
241
+ const s = await checkForUpdate();
242
+ return json({
243
+ current: s.current,
244
+ latest: s.latest,
245
+ behind: s.behind,
246
+ action: s.behind
247
+ ? "Out of date — tell the user to run: alkahest update (then restart this MCP server)."
248
+ : s.latest
249
+ ? "Up to date."
250
+ : s.reachable
251
+ ? "Nothing published on npm to compare against."
252
+ : "Couldn't reach the npm registry (offline/proxy?) — version status unknown.",
253
+ });
208
254
  });
209
- });
210
255
  // ---- comments: read map comments and act on them in-editor ----
211
256
  server.registerTool("comments", {
212
257
  title: "Map comments",
@@ -222,19 +267,19 @@ export function buildServer() {
222
267
  },
223
268
  }, async ({ path, open, project }) => {
224
269
  const root = rootOf(path);
225
- const res = await pullComments(root, { open, slug: project });
270
+ const res = await pullComments(root, withAuth({ open, slug: project }));
226
271
  if (!res.ok) {
227
- const hints = {
272
+ const hints = hinted({
228
273
  no_token: "Set ALKAHEST_TOKEN in this MCP server's config (token from alkahest.app → Account).",
229
274
  no_api: "Set ALKAHEST_API_URL in this MCP server's config.",
230
275
  no_slug: "Pass `project` — a slug from list_projects — or set ALKAHEST_PROJECT in this MCP server's config.",
231
276
  invalid_token: "The API token is invalid or revoked — create a new one at alkahest.app → Account.",
232
277
  not_found: "No accessible project for this slug.",
233
- };
278
+ });
234
279
  const hint = hints[res.code ?? ""] ? ` ${hints[res.code ?? ""]}` : "";
235
280
  return text(`Couldn't read comments (${res.code}): ${res.message}.${hint}`);
236
281
  }
237
- const map = loadMap(res.root ?? root);
282
+ const map = remote ? await getMap(undefined, res.slug) : loadMap(res.root ?? root);
238
283
  const comments = map ? enrichComments(res.comments ?? [], map) : (res.comments ?? []);
239
284
  return json({ ok: true, slug: res.slug, count: comments.length, comments });
240
285
  });
@@ -248,13 +293,13 @@ export function buildServer() {
248
293
  path: z.string().optional().describe("Project root (default: cwd)"),
249
294
  },
250
295
  }, async ({ id, resolved, path }) => {
251
- const res = await resolveComment(rootOf(path), id, resolved === undefined ? true : resolved);
296
+ const res = await resolveComment(rootOf(path), id, resolved === undefined ? true : resolved, withAuth({}));
252
297
  if (!res.ok) {
253
- const hints = {
298
+ const hints = hinted({
254
299
  no_token: "Set ALKAHEST_TOKEN in this MCP server's config.",
255
300
  forbidden: "Only the comment author or project owner can resolve it.",
256
301
  not_found: "No comment with that id.",
257
- };
302
+ });
258
303
  const hint = hints[res.code ?? ""] ? ` ${hints[res.code ?? ""]}` : "";
259
304
  return text(`Resolve failed (${res.code}): ${res.message}.${hint}`);
260
305
  }
@@ -273,19 +318,19 @@ export function buildServer() {
273
318
  },
274
319
  }, async ({ node, body, path, project }) => {
275
320
  const root = findProjectRoot(rootOf(path));
276
- const map = loadOrScan(root);
321
+ const map = remote ? await getMap(undefined, project) : loadOrScan(root);
277
322
  if (!map)
278
- return text("No map for this project — run the scan/publish tools first.");
323
+ return text(remote ? noMapMsg : "No map for this project — run the scan/publish tools first.");
279
324
  const n = resolveNode(map, node);
280
325
  if (!n)
281
326
  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 });
327
+ 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
328
  if (!res.ok) {
284
- const hints = {
329
+ const hints = hinted({
285
330
  no_token: "Set ALKAHEST_TOKEN in this MCP server's config.",
286
331
  no_slug: "Pass `project` — a slug from list_projects — or set ALKAHEST_PROJECT in this MCP server's config.",
287
332
  forbidden: "Only the project owner or a collaborator can comment.",
288
- };
333
+ });
289
334
  return text(`Add comment failed (${res.code}): ${res.message}.${hints[res.code ?? ""] ? " " + hints[res.code ?? ""] : ""}`);
290
335
  }
291
336
  return json({ ok: true, id: res.comment?.id, node_key: n.node_key, anchor_label: n.anchor_label });
@@ -300,49 +345,50 @@ export function buildServer() {
300
345
  path: z.string().optional().describe("Project root (default: cwd)"),
301
346
  },
302
347
  }, async ({ id, body, path }) => {
303
- const res = await postComment(rootOf(path), { parent_id: id, body });
348
+ const res = await postComment(rootOf(path), withAuth({ parent_id: id, body }));
304
349
  if (!res.ok) {
305
- const hints = {
350
+ const hints = hinted({
306
351
  no_token: "Set ALKAHEST_TOKEN in this MCP server's config.",
307
352
  not_found: "No comment with that id (parent).",
308
353
  forbidden: "Only the project owner or a collaborator can comment.",
309
- };
354
+ });
310
355
  return text(`Reply failed (${res.code}): ${res.message}.${hints[res.code ?? ""] ? " " + hints[res.code ?? ""] : ""}`);
311
356
  }
312
357
  return json({ ok: true, id: res.comment?.id, parent_id: id });
313
358
  });
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
- });
359
+ if (!remote)
360
+ server.registerTool("comment_to_issue", {
361
+ title: "File map comments as a GitHub issue",
362
+ description: "Group one or more map comments (ids from the comments tool) into a SINGLE GitHub issue and link it back onto each. " +
363
+ "Creates the issue with the local `gh` CLI (must be installed and authenticated; it runs in the project's git repo), " +
364
+ "then records the issue URL on the comments so the hosted viewer shows a 'tracked' badge. Use this to turn feedback " +
365
+ "into tracked work. Needs an API token; owner or collaborator only. Pass force:true to re-file comments that are " +
366
+ "already linked to an issue (creates a new one).",
367
+ inputSchema: {
368
+ ids: z.array(z.string()).min(1).describe("Comment ids to group into one issue (from the comments tool)"),
369
+ project: z.string().optional().describe("Which project (slug) — say it explicitly when the folder isn't a linked checkout. List them with list_projects."),
370
+ path: z.string().optional().describe("Project root (default: cwd)"),
371
+ title: z.string().optional().describe("Issue title (else derived from the comments)"),
372
+ repo: z.string().optional().describe("Target GitHub repo owner/repo (else gh's default for the repo)"),
373
+ force: z.boolean().optional().describe("File even if some selected comments are already tracked"),
374
+ },
375
+ }, async ({ ids, path, title, repo, force, project }) => {
376
+ const res = await fileCommentsIssue(rootOf(path), ids, { title, repo, force, slug: project });
377
+ if (!res.ok) {
378
+ const hints = {
379
+ no_token: "Set ALKAHEST_TOKEN in this MCP server's config.",
380
+ no_slug: "Pass `project` a slug from list_projects or set ALKAHEST_PROJECT in this MCP server's config.",
381
+ already_tracked: "Some comments already have an issue pass force:true to file a new one.",
382
+ gh_failed: "Install and authenticate the GitHub CLI (`gh auth login`) for this repo.",
383
+ forbidden: "Only the project owner or a collaborator can file issues.",
384
+ not_found: "One or more ids don't exist — list them with the comments tool.",
385
+ };
386
+ return text(`File issue failed (${res.code}): ${res.message}.${hints[res.code ?? ""] ? " " + hints[res.code ?? ""] : ""}`);
387
+ }
388
+ return json({ ok: true, issue_url: res.issue_url, ids: res.ids, title: res.title });
389
+ });
344
390
  // ---- issues: the Issue Map — a map-shaped issue tracker on the hosted viewer ----
345
- const issueHints = {
391
+ const issueHints = hinted({
346
392
  no_token: "Set ALKAHEST_TOKEN in this MCP server's config (token from alkahest.app → Account).",
347
393
  no_api: "Set ALKAHEST_API_URL in this MCP server's config.",
348
394
  no_slug: "Pass `project` — a slug from list_projects — or set ALKAHEST_PROJECT in this MCP server's config.",
@@ -350,7 +396,7 @@ export function buildServer() {
350
396
  forbidden: "Only the project owner or a collaborator can write issues.",
351
397
  not_found: "Not found — list ids with the issues tool, or the project's issue maps with the maps tool.",
352
398
  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
- };
399
+ });
354
400
  // `maps` (present on ambiguous_map / unknown-slug) is appended as JSON so the agent can pick a map
355
401
  // without a second round-trip to the maps tool.
356
402
  const issueFail = (what, code, message, maps) => text(`${what} failed (${code}): ${message}.${issueHints[code ?? ""] ? " " + issueHints[code ?? ""] : ""}${maps?.length ? ` Maps: ${JSON.stringify(maps)}` : ""}`);
@@ -372,7 +418,7 @@ export function buildServer() {
372
418
  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
419
  },
374
420
  }, async ({ path, open, map, q, project }) => {
375
- const res = await pullIssues(rootOf(path), { mapSlug: map, q, slug: project });
421
+ const res = await pullIssues(rootOf(path), withAuth({ mapSlug: map, q, slug: project }));
376
422
  if (!res.ok || !res.graph)
377
423
  return issueFail("Read issues", res.code, res.message, res.maps);
378
424
  const states = deriveIssueStates(res.graph);
@@ -422,7 +468,7 @@ export function buildServer() {
422
468
  target_key: target,
423
469
  }
424
470
  : {};
425
- const res = await createIssue(rootOf(path), { title, type, status, body, priority, due_on, assignee_id, props, parent_id, mapSlug: map, slug: project, ...targetFields });
471
+ 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
472
  if (!res.ok || !res.issue)
427
473
  return issueFail("Add issue", res.code, res.message, res.maps);
428
474
  return json({ ok: true, issue: res.issue });
@@ -463,7 +509,7 @@ export function buildServer() {
463
509
  path: z.string().optional().describe("Project root (default: cwd — used only to find your token/API)"),
464
510
  },
465
511
  }, async ({ status, project, q, path }) => {
466
- const res = await pullTasks(rootOf(path), { status, project, q });
512
+ const res = await pullTasks(rootOf(path), withAuth({ status, project, q }));
467
513
  if (!res.ok || !res.tasks)
468
514
  return issueFail("List tasks", res.code, res.message);
469
515
  return json({ ok: true, count: res.tasks.length, tasks: res.tasks });
@@ -510,7 +556,7 @@ export function buildServer() {
510
556
  path: z.string().optional().describe("Project root (default: cwd — a linked checkout auto-tags its project)"),
511
557
  },
512
558
  }, 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 });
559
+ const res = await createTask(rootOf(path), withAuth({ title, body, slug: project, workspace, tags, due_on, dedup_key, note_mode, note, skill }));
514
560
  if (!res.ok || !res.task) {
515
561
  const wsHint = res.workspaces?.length ? ` Workspaces: ${JSON.stringify(res.workspaces)}` : "";
516
562
  return issueFail("Add task", res.code, `${res.message ?? ""}${wsHint}`);
@@ -531,7 +577,7 @@ export function buildServer() {
531
577
  path: z.string().optional().describe("Project root (default: cwd — used only to find your token/API)"),
532
578
  },
533
579
  }, async ({ id, reopen, path }) => {
534
- const res = await completeTask(rootOf(path), { id, reopen });
580
+ const res = await completeTask(rootOf(path), withAuth({ id, reopen }));
535
581
  if (!res.ok || !res.task)
536
582
  return issueFail(reopen ? "Reopen task" : "Complete task", res.code, res.message);
537
583
  return json({ ok: true, task: res.task });
@@ -556,7 +602,7 @@ export function buildServer() {
556
602
  path: z.string().optional().describe("Project root (default: cwd — used only to find your token/API)"),
557
603
  },
558
604
  }, async ({ id, title, body, due_on, tags, note_mode, note, skill, path }) => {
559
- const res = await updateTask(rootOf(path), {
605
+ const res = await updateTask(rootOf(path), withAuth({
560
606
  id,
561
607
  title,
562
608
  body: body === "" ? null : body,
@@ -565,7 +611,7 @@ export function buildServer() {
565
611
  note_mode: note_mode === "" ? null : note_mode,
566
612
  note: note === "" ? null : note,
567
613
  skill: skill === "" ? null : skill,
568
- });
614
+ }));
569
615
  if (!res.ok || !res.task)
570
616
  return issueFail("Update task", res.code, res.message);
571
617
  return json({ ok: true, task: res.task });
@@ -586,7 +632,7 @@ export function buildServer() {
586
632
  path: z.string().optional().describe("Project root (default: cwd — used only to find your token/API)"),
587
633
  },
588
634
  }, async ({ path }) => {
589
- const res = await pullSkills(rootOf(path));
635
+ const res = await pullSkills(rootOf(path), withAuth({}));
590
636
  if (!res.ok || !res.skills)
591
637
  return issueFail("List skills", res.code, res.message);
592
638
  return json({ ok: true, count: res.skills.length, skills: res.skills });
@@ -613,7 +659,7 @@ export function buildServer() {
613
659
  path: z.string().optional().describe("Project root (default: cwd — used only to find your token/API)"),
614
660
  },
615
661
  }, async ({ name, body, default_for, rename_from, path }) => {
616
- const res = await saveSkill(rootOf(path), { name, body, default_for, rename_from });
662
+ const res = await saveSkill(rootOf(path), withAuth({ name, body, default_for, rename_from }));
617
663
  if (!res.ok || !res.skill)
618
664
  return issueFail("Add skill", res.code, res.message);
619
665
  return json({ ok: true, skill: res.skill });
@@ -634,7 +680,7 @@ export function buildServer() {
634
680
  path: z.string().optional().describe("Project root (default: cwd — used only to find your token/API)"),
635
681
  },
636
682
  }, async ({ task, open, path }) => {
637
- const res = await pullTaskComments(rootOf(path), { task, open });
683
+ const res = await pullTaskComments(rootOf(path), withAuth({ task, open }));
638
684
  if (!res.ok || !res.comments)
639
685
  return issueFail("Read task comments", res.code, res.message);
640
686
  return json({ ok: true, count: res.comments.length, comments: res.comments });
@@ -652,7 +698,7 @@ export function buildServer() {
652
698
  path: z.string().optional().describe("Project root (default: cwd — used only to find your token/API)"),
653
699
  },
654
700
  }, async ({ task, body, path }) => {
655
- const res = await postTaskComment(rootOf(path), { task_id: task, body, kind: "question" });
701
+ const res = await postTaskComment(rootOf(path), withAuth({ task_id: task, body, kind: "question" }));
656
702
  if (!res.ok || !res.comment)
657
703
  return issueFail("Ask task", res.code, res.message);
658
704
  return json({ ok: true, comment: res.comment, note: "Question posted — re-check with task_comments, then resolve_task_comment once answered." });
@@ -673,7 +719,7 @@ export function buildServer() {
673
719
  path: z.string().optional().describe("Project root (default: cwd — used only to find your token/API)"),
674
720
  },
675
721
  }, async ({ task, parent, body, kind, path }) => {
676
- const res = await postTaskComment(rootOf(path), { task_id: task, parent, body, kind });
722
+ const res = await postTaskComment(rootOf(path), withAuth({ task_id: task, parent, body, kind }));
677
723
  if (!res.ok || !res.comment)
678
724
  return issueFail("Comment on task", res.code, res.message);
679
725
  return json({ ok: true, comment: res.comment });
@@ -690,7 +736,7 @@ export function buildServer() {
690
736
  path: z.string().optional().describe("Project root (default: cwd — used only to find your token/API)"),
691
737
  },
692
738
  }, async ({ id, resolved, path }) => {
693
- const res = await resolveTaskComment(rootOf(path), { id, resolved });
739
+ const res = await resolveTaskComment(rootOf(path), withAuth({ id, resolved }));
694
740
  if (!res.ok)
695
741
  return issueFail("Resolve task comment", res.code, res.message);
696
742
  return json({ ok: true, id: res.id, resolved: res.resolved });
@@ -713,9 +759,9 @@ export function buildServer() {
713
759
  }, async ({ q, path, project }) => {
714
760
  const root = rootOf(path);
715
761
  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 }),
762
+ pullNotes(root, withAuth({ q, bodies: "excerpt", slug: project })),
763
+ pullIssues(root, withAuth({ q, slug: project })),
764
+ pullTasks(root, withAuth({ q, status: "all", project })),
719
765
  ]);
720
766
  const notes = notesRes.ok && notesRes.maps
721
767
  ? 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 +796,7 @@ export function buildServer() {
750
796
  full_bodies: z.boolean().optional().describe("Return complete bodies instead of 240-char excerpts (heavy on a big wiki)"),
751
797
  },
752
798
  }, async ({ path, q, map, full_bodies, project }) => {
753
- const res = await pullNotes(rootOf(path), { mapSlug: map, q, bodies: full_bodies ? undefined : "excerpt", slug: project });
799
+ const res = await pullNotes(rootOf(path), withAuth({ mapSlug: map, q, bodies: full_bodies ? undefined : "excerpt", slug: project }));
754
800
  if (!res.ok || !res.maps)
755
801
  return issueFail("Read notes", res.code, res.message, res.mapList);
756
802
  return json({ ok: true, project: res.project, count: res.maps.reduce((n, m) => n + m.notes.length, 0), maps: res.maps });
@@ -766,7 +812,7 @@ export function buildServer() {
766
812
  path: z.string().optional().describe("Project root (default: cwd)"),
767
813
  },
768
814
  }, async ({ note, map, path, project }) => {
769
- const res = await getNote(rootOf(path), { note, mapSlug: map, slug: project });
815
+ const res = await getNote(rootOf(path), withAuth({ note, mapSlug: map, slug: project }));
770
816
  if (!res.ok || !res.note)
771
817
  return issueFail("Get note", res.code, res.message, res.mapList);
772
818
  const { ok: _ok, code: _code, message: _message, mapList: _ml, ...rest } = res;
@@ -790,7 +836,7 @@ export function buildServer() {
790
836
  path: z.string().optional().describe("Project root (default: cwd)"),
791
837
  },
792
838
  }, 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 });
839
+ const res = await createNote(rootOf(path), withAuth({ title, body, note_slug, folder, props, mapSlug: map, slug: project }));
794
840
  if (!res.ok || !res.note)
795
841
  return issueFail("Add note", res.code, res.message, res.maps);
796
842
  return json({ ok: true, note: res.note });
@@ -811,7 +857,7 @@ export function buildServer() {
811
857
  path: z.string().optional().describe("Project root (default: cwd)"),
812
858
  },
813
859
  }, async ({ from, to, remove, path, project }) => {
814
- const res = await linkNotes(rootOf(path), { from, to, remove, slug: project });
860
+ const res = await linkNotes(rootOf(path), withAuth({ from, to, remove, slug: project }));
815
861
  if (!res.ok)
816
862
  return issueFail(remove ? "Unlink notes" : "Link notes", res.code, res.message, res.maps);
817
863
  return json({ ok: true, ...(remove ? { removed: `${from} → ${to}` } : { linked: `${from} → ${to}` }) });
@@ -829,7 +875,7 @@ export function buildServer() {
829
875
  path: z.string().optional().describe("Project root (default: cwd)"),
830
876
  },
831
877
  }, async ({ note, map, path, project }) => {
832
- const res = await mapNote(rootOf(path), { noteRef: note, mapSlug: map, slug: project });
878
+ const res = await mapNote(rootOf(path), withAuth({ noteRef: note, mapSlug: map, slug: project }));
833
879
  if (!res.ok)
834
880
  return issueFail("Move note", res.code, res.message, res.maps);
835
881
  return json({ ok: true, note: res.note, map: res.map });
@@ -859,7 +905,7 @@ export function buildServer() {
859
905
  path: z.string().optional().describe("Project root (default: cwd)"),
860
906
  },
861
907
  }, 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 });
908
+ const res = await updateNote(rootOf(path), withAuth({ note, title, body, new_slug, folder, props, delete: del, reason, restore, mapSlug: map, slug: project }));
863
909
  const what = del ? "Delete note" : restore ? "Restore note" : "Update note";
864
910
  if (!res.ok)
865
911
  return issueFail(what, res.code, res.message, res.maps);
@@ -899,7 +945,7 @@ export function buildServer() {
899
945
  if (!define?.length && !remove?.length) {
900
946
  return json({ ok: false, error: "bad_request", message: "Pass `define` (definitions to register) and/or `remove` (keys to unregister)." });
901
947
  }
902
- const res = await editPropDefs(rootOf(path), { defs: define, remove, mapSlug: map, slug: project });
948
+ const res = await editPropDefs(rootOf(path), withAuth({ defs: define, remove, mapSlug: map, slug: project }));
903
949
  if (!res.ok)
904
950
  return issueFail("Edit note props", res.code, res.message, res.maps);
905
951
  return json({ ok: true, added: res.added ?? 0, merged: res.merged ?? 0, removed: res.removed ?? 0, skipped: res.skipped ?? 0 });
@@ -916,7 +962,7 @@ export function buildServer() {
916
962
  type: z.enum(["code", "issue", "note"]).optional().describe("Restrict to one type (default: all)"),
917
963
  },
918
964
  }, async ({ path, type, project }) => {
919
- const res = await listMaps(rootOf(path), { type, slug: project });
965
+ const res = await listMaps(rootOf(path), withAuth({ type, slug: project }));
920
966
  if (!res.ok || !res.maps)
921
967
  return issueFail("List maps", res.code, res.message);
922
968
  return json({ ok: true, slug: res.slug, count: res.maps.length, maps: res.maps });
@@ -932,10 +978,10 @@ export function buildServer() {
932
978
  "(screens/resources counts) so you can match a local scan by structure. Needs an API token; no project context.",
933
979
  inputSchema: {},
934
980
  }, async () => {
935
- const res = await listProjects({});
981
+ const res = await listProjects(withAuth({}));
936
982
  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}`);
983
+ const h = hinted({ no_token: "Set ALKAHEST_TOKEN in this MCP server's config." })[res.code ?? ""];
984
+ return text(`List projects failed (${res.code}): ${res.message}.${h ? ` ${h}` : ""}`);
939
985
  }
940
986
  return json({
941
987
  ok: true,
@@ -966,7 +1012,7 @@ export function buildServer() {
966
1012
  limit: z.number().optional().describe("Max versions (default 50)"),
967
1013
  },
968
1014
  }, async ({ path, map, limit, project }) => {
969
- const res = await listHistory(rootOf(path), { map, limit, slug: project });
1015
+ const res = await listHistory(rootOf(path), withAuth({ map, limit, slug: project }));
970
1016
  if (!res.ok || !res.versions)
971
1017
  return issueFail("History", res.code, res.message);
972
1018
  // Newest first; include count deltas vs the previous version so the agent needn't recompute.
@@ -1000,7 +1046,7 @@ export function buildServer() {
1000
1046
  path: z.string().optional().describe("Project root (default: cwd)"),
1001
1047
  },
1002
1048
  }, async ({ slug, type, name, path, project }) => {
1003
- const res = await createMap(rootOf(path), { mapSlug: slug, type, mapName: name, slug: project });
1049
+ const res = await createMap(rootOf(path), withAuth({ mapSlug: slug, type, mapName: name, slug: project }));
1004
1050
  if (!res.ok || !res.map)
1005
1051
  return issueFail("Create map", res.code, res.message);
1006
1052
  return json({ ok: true, map: res.map });
@@ -1050,7 +1096,7 @@ export function buildServer() {
1050
1096
  set.target_key = target;
1051
1097
  }
1052
1098
  }
1053
- const res = await updateIssue(rootOf(path), { id, ...(del ? { delete: true } : { set }) });
1099
+ const res = await updateIssue(rootOf(path), withAuth({ id, ...(del ? { delete: true } : { set }) }));
1054
1100
  if (!res.ok)
1055
1101
  return issueFail("Update issue", res.code, res.message);
1056
1102
  return json(res.deleted ? { ok: true, deleted: true, id } : { ok: true, issue: res.issue });
@@ -1069,7 +1115,7 @@ export function buildServer() {
1069
1115
  },
1070
1116
  }, async ({ from, to, kind, remove, path }) => {
1071
1117
  const edge = [{ to, kind: kind ?? "blocks" }];
1072
- const res = await updateIssue(rootOf(path), { id: from, ...(remove ? { remove_edges: edge } : { add_edges: edge }) });
1118
+ const res = await updateIssue(rootOf(path), withAuth({ id: from, ...(remove ? { remove_edges: edge } : { add_edges: edge }) }));
1073
1119
  if (!res.ok)
1074
1120
  return issueFail("Link issues", res.code, res.message);
1075
1121
  return json({ ok: true, from, to, kind: kind ?? "blocks", removed: Boolean(remove) });
@@ -1088,7 +1134,7 @@ export function buildServer() {
1088
1134
  path: z.string().optional().describe("Project root (default: cwd)"),
1089
1135
  },
1090
1136
  }, async ({ issue, map, remove, path, project }) => {
1091
- const res = await mapIssue(rootOf(path), { issueId: issue, mapSlug: map, remove, slug: project });
1137
+ const res = await mapIssue(rootOf(path), withAuth({ issueId: issue, mapSlug: map, remove, slug: project }));
1092
1138
  if (!res.ok)
1093
1139
  return issueFail(remove ? "Unmap issue" : "Map issue", res.code, res.message, res.maps);
1094
1140
  return json({ ok: true, issue: res.issue, map: res.map, member: res.member });
@@ -1108,7 +1154,7 @@ export function buildServer() {
1108
1154
  path: z.string().optional().describe("Project root (default: cwd)"),
1109
1155
  },
1110
1156
  }, async ({ issue, open, path, project }) => {
1111
- const res = await pullIssueComments(rootOf(path), { issue, open, slug: project });
1157
+ const res = await pullIssueComments(rootOf(path), withAuth({ issue, open, slug: project }));
1112
1158
  if (!res.ok || !res.comments)
1113
1159
  return issueFail("Read issue comments", res.code, res.message);
1114
1160
  return json({ ok: true, count: res.comments.length, comments: res.comments });
@@ -1129,7 +1175,7 @@ export function buildServer() {
1129
1175
  path: z.string().optional().describe("Project root (default: cwd)"),
1130
1176
  },
1131
1177
  }, async ({ issue, body, mention, path }) => {
1132
- const res = await postIssueComment(rootOf(path), { issue_id: issue, body, kind: "question", mention });
1178
+ const res = await postIssueComment(rootOf(path), withAuth({ issue_id: issue, body, kind: "question", mention }));
1133
1179
  if (!res.ok || !res.comment)
1134
1180
  return issueFail("Ask issue", res.code, res.message);
1135
1181
  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 +1195,7 @@ export function buildServer() {
1149
1195
  path: z.string().optional().describe("Project root (default: cwd)"),
1150
1196
  },
1151
1197
  }, async ({ issue, parent, body, kind, mention, path }) => {
1152
- const res = await postIssueComment(rootOf(path), { issue_id: issue, parent, body, kind, mention });
1198
+ const res = await postIssueComment(rootOf(path), withAuth({ issue_id: issue, parent, body, kind, mention }));
1153
1199
  if (!res.ok || !res.comment)
1154
1200
  return issueFail("Reply on issue", res.code, res.message);
1155
1201
  return json({ ok: true, comment: res.comment });
@@ -1165,7 +1211,7 @@ export function buildServer() {
1165
1211
  path: z.string().optional().describe("Project root (default: cwd)"),
1166
1212
  },
1167
1213
  }, async ({ id, resolved, path }) => {
1168
- const res = await resolveIssueComment(rootOf(path), { id, resolved });
1214
+ const res = await resolveIssueComment(rootOf(path), withAuth({ id, resolved }));
1169
1215
  if (!res.ok)
1170
1216
  return issueFail("Resolve question", res.code, res.message);
1171
1217
  return json({ ok: true, id: res.id, resolved: res.resolved });
@@ -1189,7 +1235,7 @@ export function buildServer() {
1189
1235
  // Resolve the terminal status to move to (explicit, else the project's first terminal status).
1190
1236
  let target = status;
1191
1237
  if (!target) {
1192
- const g = await pullIssues(root, { slug: project });
1238
+ const g = await pullIssues(root, withAuth({ slug: project }));
1193
1239
  if (!g.ok || !g.graph)
1194
1240
  return issueFail("Complete issue", g.code, g.message, g.maps);
1195
1241
  const terminal = [...terminalStatuses(g.graph.issue_config)];
@@ -1197,10 +1243,10 @@ export function buildServer() {
1197
1243
  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
1244
  target = terminal[0];
1199
1245
  }
1200
- const upd = await updateIssue(root, { id, set: { status: target } });
1246
+ const upd = await updateIssue(root, withAuth({ id, set: { status: target } }));
1201
1247
  if (!upd.ok)
1202
1248
  return issueFail("Complete issue", upd.code, upd.message);
1203
- const cmt = await postIssueComment(root, { issue_id: id, body: result, kind: "result" });
1249
+ const cmt = await postIssueComment(root, withAuth({ issue_id: id, body: result, kind: "result" }));
1204
1250
  if (!cmt.ok)
1205
1251
  return issueFail("Complete issue (result note)", cmt.code, cmt.message);
1206
1252
  return json({