@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.
- package/dist/core/mapFetch.d.ts +14 -0
- package/dist/core/mapFetch.js +24 -0
- package/dist/core/mapFetch.js.map +1 -0
- package/dist/mcp/server.d.ts +15 -1
- package/dist/mcp/server.js +269 -223
- package/dist/mcp/server.js.map +1 -1
- package/package.json +8 -1
package/dist/mcp/server.js
CHANGED
|
@@ -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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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: {
|
|
53
|
-
|
|
54
|
-
|
|
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(
|
|
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: {
|
|
80
|
-
|
|
81
|
-
|
|
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(
|
|
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: {
|
|
94
|
-
|
|
95
|
-
|
|
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(
|
|
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
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
"
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
"
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
"
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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 reached — upgrade to Pro for more.",
|
|
193
|
+
invalid_token: "The API token is invalid or revoked — create 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
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
"
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
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
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
"
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
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
|
|
938
|
-
return text(`List projects failed (${res.code}): ${res.message}.${
|
|
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({
|