@honua/mcp-server 0.1.4-beta.0 → 0.1.9-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/README.md +30 -2
  2. package/dist/src/certification/admin-parity.d.ts +96 -0
  3. package/dist/src/certification/admin-parity.d.ts.map +1 -0
  4. package/dist/src/certification/admin-parity.js +235 -0
  5. package/dist/src/certification/target.d.ts.map +1 -1
  6. package/dist/src/certification/target.js +18 -26
  7. package/dist/src/credential-endpoint.d.ts +3 -0
  8. package/dist/src/credential-endpoint.d.ts.map +1 -0
  9. package/dist/src/credential-endpoint.js +24 -0
  10. package/dist/src/entrypoint.d.ts +20 -0
  11. package/dist/src/entrypoint.d.ts.map +1 -0
  12. package/dist/src/entrypoint.js +38 -0
  13. package/dist/src/index.d.ts +22 -1
  14. package/dist/src/index.d.ts.map +1 -1
  15. package/dist/src/index.js +33 -5
  16. package/dist/src/proxy.d.ts.map +1 -1
  17. package/dist/src/proxy.js +31 -16
  18. package/dist/src/release/zero-to-map-checkpoint.d.ts +34 -0
  19. package/dist/src/release/zero-to-map-checkpoint.d.ts.map +1 -0
  20. package/dist/src/release/zero-to-map-checkpoint.js +369 -0
  21. package/dist/src/release/zero-to-map-cli.d.ts +51 -0
  22. package/dist/src/release/zero-to-map-cli.d.ts.map +1 -0
  23. package/dist/src/release/zero-to-map-cli.js +881 -0
  24. package/dist/src/release/zero-to-map-provision.d.ts +37 -0
  25. package/dist/src/release/zero-to-map-provision.d.ts.map +1 -0
  26. package/dist/src/release/zero-to-map-provision.js +177 -0
  27. package/dist/src/release/zero-to-map.d.ts +296 -0
  28. package/dist/src/release/zero-to-map.d.ts.map +1 -0
  29. package/dist/src/release/zero-to-map.js +1069 -0
  30. package/dist/src/tools/admin-install-local.d.ts +30 -0
  31. package/dist/src/tools/admin-install-local.d.ts.map +1 -0
  32. package/dist/src/tools/admin-install-local.js +22 -0
  33. package/dist/src/tools/control-plane-command.d.ts +99 -0
  34. package/dist/src/tools/control-plane-command.d.ts.map +1 -0
  35. package/dist/src/tools/control-plane-command.js +180 -0
  36. package/dist/src/tools/docs-search.d.ts +100 -0
  37. package/dist/src/tools/docs-search.d.ts.map +1 -0
  38. package/dist/src/tools/docs-search.js +381 -0
  39. package/package.json +8 -3
  40. package/release/zero-to-map/README.md +174 -0
  41. package/release/zero-to-map/configs/claude-code.mcp.json +11 -0
  42. package/release/zero-to-map/configs/claude-desktop.json +11 -0
  43. package/release/zero-to-map/configs/cursor.json +11 -0
  44. package/release/zero-to-map/contracts/aws-ecs-provision-binding.schema.json +59 -0
  45. package/release/zero-to-map/contracts/checkpoint.schema.json +152 -0
  46. package/release/zero-to-map/contracts/console-receipt.schema.json +150 -0
  47. package/release/zero-to-map/contracts/esri-gp-mcp.v1.json +73 -0
  48. package/release/zero-to-map/fixtures/parcels.geojson +33 -0
  49. package/release/zero-to-map/fixtures/zoning.geojson +24 -0
  50. package/release/zero-to-map/journey.v1.json +1248 -0
@@ -0,0 +1,381 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { z } from "zod";
6
+ import { capabilityUnavailable } from "../capability.js";
7
+ import { jsonText } from "../helpers.js";
8
+ /**
9
+ * `honua_docs_search` — answer a documentation question from the repository's
10
+ * own versioned documentation corpus, with citations.
11
+ *
12
+ * The corpus is `llms-full.txt` (generated by `npm run docs:llms`), which
13
+ * concatenates the documents indexed in `llms.txt` under `# File: <path>`
14
+ * headers. Every hit therefore carries the exact source file it came from and
15
+ * the commit those bytes are at (so the citation keeps naming the text that was
16
+ * actually searched once `trunk` moves), and the version block carries the
17
+ * release that documentation describes — derived
18
+ * from `docs/versions.json` when it has been generated
19
+ * (`npm run docs:versions:write`) and otherwise directly from `package.json` +
20
+ * `.release-please-manifest.json`, the same authoritative pair
21
+ * `scripts/docs-versions.mjs` uses. The policy those citations follow is
22
+ * documented in `docs/documentation-versions.md`.
23
+ *
24
+ * This is a LOCAL tool: it reads committed files and never makes a network
25
+ * request. When the corpus cannot be located (for example inside an installed
26
+ * `@honua/mcp-server` that does not ship the corpus), it degrades the same way
27
+ * every other surface-dependent tool does — a structured `available: false`
28
+ * result with guidance, not a crash and not a misleading empty answer.
29
+ */
30
+ const REPOSITORY = "honua-io/honua-sdk-js";
31
+ const SITE_URL = "https://honua-io.github.io/honua-sdk-js/";
32
+ const CORPUS_FILE = "llms-full.txt";
33
+ const MAX_EXCERPT_CHARS = 1200;
34
+ const EXCERPT_CONTEXT_LINES = 6;
35
+ const COMMIT_PATTERN = /^[0-9a-f]{40}$/;
36
+ /**
37
+ * Function words carry no retrieval signal but appear in every document, so on
38
+ * a natural-language question ("how do I handle a capability error") they match
39
+ * everywhere, add frequency weight everywhere, and inflate the matched-term
40
+ * multiplier everywhere -- which is exactly enough to push `docs/errors.md`
41
+ * below `docs/guide.md` and out of the default three results. Terms shorter
42
+ * than two characters are already dropped by the tokenizer.
43
+ */
44
+ const STOPWORDS = new Set([
45
+ "about",
46
+ "all",
47
+ "an",
48
+ "and",
49
+ "any",
50
+ "are",
51
+ "as",
52
+ "at",
53
+ "be",
54
+ "been",
55
+ "but",
56
+ "by",
57
+ "can",
58
+ "do",
59
+ "does",
60
+ "doing",
61
+ "done",
62
+ "for",
63
+ "from",
64
+ "get",
65
+ "getting",
66
+ "has",
67
+ "have",
68
+ "how",
69
+ "if",
70
+ "in",
71
+ "into",
72
+ "is",
73
+ "it",
74
+ "its",
75
+ "me",
76
+ "my",
77
+ "not",
78
+ "of",
79
+ "on",
80
+ "onto",
81
+ "or",
82
+ "our",
83
+ "should",
84
+ "so",
85
+ "some",
86
+ "that",
87
+ "the",
88
+ "their",
89
+ "them",
90
+ "then",
91
+ "there",
92
+ "these",
93
+ "they",
94
+ "this",
95
+ "to",
96
+ "up",
97
+ "use",
98
+ "used",
99
+ "using",
100
+ "via",
101
+ "want",
102
+ "was",
103
+ "we",
104
+ "what",
105
+ "when",
106
+ "where",
107
+ "which",
108
+ "who",
109
+ "why",
110
+ "will",
111
+ "with",
112
+ "would",
113
+ "you",
114
+ "your",
115
+ ]);
116
+ export const schema = z.object({
117
+ query: z
118
+ .string()
119
+ .min(2)
120
+ .describe("Natural-language or keyword documentation question, e.g. 'how do I handle a capability error'"),
121
+ limit: z.number().int().min(1).max(10).optional().describe("Maximum documents to return (default 3)"),
122
+ pathPrefix: z
123
+ .string()
124
+ .optional()
125
+ .describe("Restrict results to source files whose path starts with this value, e.g. 'docs/' or 'mcp/'"),
126
+ });
127
+ /**
128
+ * Split `llms-full.txt` into its constituent documents. The generator emits
129
+ * `# File: <path>` as the delimiter for each concatenated document.
130
+ */
131
+ export function splitCorpus(corpus) {
132
+ const documents = [];
133
+ const pattern = /^# File: (.+)$/gm;
134
+ const matches = [...corpus.matchAll(pattern)];
135
+ for (const [index, match] of matches.entries()) {
136
+ const start = (match.index ?? 0) + match[0].length;
137
+ const end = index + 1 < matches.length ? (matches[index + 1].index ?? corpus.length) : corpus.length;
138
+ const body = corpus
139
+ .slice(start, end)
140
+ .replace(/\n-{3,}\s*$/, "")
141
+ .trim();
142
+ const heading = /^#\s+(.+)$/m.exec(body);
143
+ documents.push({ file: match[1].trim(), title: heading?.[1]?.trim() ?? match[1].trim(), body });
144
+ }
145
+ return documents;
146
+ }
147
+ /**
148
+ * The terms a query is actually ranked and excerpted on: distinct tokens with
149
+ * the stopwords removed. A query made entirely of stopwords ("how do I") keeps
150
+ * its raw tokens rather than degrading to "no indexed document matched", so
151
+ * filtering can only ever improve a ranking, never empty one.
152
+ */
153
+ export function queryTermsOf(query) {
154
+ const tokens = [...new Set(query.toLowerCase().match(/[a-z0-9_./-]{2,}/g) ?? [])];
155
+ const significant = tokens.filter((token) => !STOPWORDS.has(token));
156
+ return significant.length > 0 ? significant : tokens;
157
+ }
158
+ function countOccurrences(haystack, needle) {
159
+ let count = 0;
160
+ let index = haystack.indexOf(needle);
161
+ while (index !== -1) {
162
+ count += 1;
163
+ index = haystack.indexOf(needle, index + needle.length);
164
+ }
165
+ return count;
166
+ }
167
+ /** Deterministic lexical score: path and heading matches outweigh body matches. */
168
+ export function scoreDocument(document, queryTerms) {
169
+ const file = document.file.toLowerCase();
170
+ const body = document.body.toLowerCase();
171
+ const headings = (document.body.match(/^#{1,4} .+$/gm) ?? []).join("\n").toLowerCase();
172
+ let score = 0;
173
+ let matched = 0;
174
+ for (const term of queryTerms) {
175
+ const inBody = countOccurrences(body, term);
176
+ const inHeadings = countOccurrences(headings, term);
177
+ const inPath = countOccurrences(file, term);
178
+ if (inBody + inHeadings + inPath > 0)
179
+ matched += 1;
180
+ score += Math.min(inBody, 20) + inHeadings * 3 + inPath * 8;
181
+ }
182
+ // Require more than a single incidental term hit on multi-term queries.
183
+ if (matched === 0)
184
+ return 0;
185
+ return score * matched;
186
+ }
187
+ /** The most relevant window of a document, trimmed to a token-friendly size. */
188
+ export function excerptFor(document, queryTerms) {
189
+ const lines = document.body.split("\n");
190
+ let bestLine = 0;
191
+ let bestHits = 0;
192
+ for (const [index, line] of lines.entries()) {
193
+ const lower = line.toLowerCase();
194
+ const hits = queryTerms.reduce((total, term) => total + countOccurrences(lower, term), 0);
195
+ if (hits > bestHits) {
196
+ bestHits = hits;
197
+ bestLine = index;
198
+ }
199
+ }
200
+ const start = Math.max(0, bestLine - EXCERPT_CONTEXT_LINES);
201
+ const end = Math.min(lines.length, bestLine + EXCERPT_CONTEXT_LINES + 1);
202
+ const excerpt = lines.slice(start, end).join("\n").trim();
203
+ return excerpt.length > MAX_EXCERPT_CHARS ? `${excerpt.slice(0, MAX_EXCERPT_CHARS)}…` : excerpt;
204
+ }
205
+ /**
206
+ * Locate the corpus: an explicit `HONUA_DOCS_CORPUS_PATH` (the corpus file
207
+ * itself, or a directory holding one), otherwise the nearest ancestor directory
208
+ * holding `llms-full.txt`.
209
+ *
210
+ * The override names the file to read, so it is read — its basename is not
211
+ * assumed to be `llms-full.txt`. Discarding it and reading `<parent>/llms-full.txt`
212
+ * instead would throw `ENOENT` for a corpus under any other name, or silently
213
+ * answer from a different sibling file.
214
+ */
215
+ export function resolveDocsCorpus(env = process.env, from) {
216
+ const override = env.HONUA_DOCS_CORPUS_PATH;
217
+ if (override) {
218
+ const resolved = path.resolve(override);
219
+ if (!fs.existsSync(resolved))
220
+ return undefined;
221
+ const corpusPath = fs.statSync(resolved).isDirectory() ? path.join(resolved, CORPUS_FILE) : resolved;
222
+ if (!fs.existsSync(corpusPath))
223
+ return undefined;
224
+ return { corpusPath, root: path.dirname(corpusPath) };
225
+ }
226
+ let dir = from ?? path.dirname(fileURLToPath(import.meta.url));
227
+ for (let depth = 0; depth < 10; depth += 1) {
228
+ const corpusPath = path.join(dir, CORPUS_FILE);
229
+ if (fs.existsSync(corpusPath))
230
+ return { corpusPath, root: dir };
231
+ const parent = path.dirname(dir);
232
+ if (parent === dir)
233
+ break;
234
+ dir = parent;
235
+ }
236
+ return undefined;
237
+ }
238
+ /** The documentation root holding the corpus, or `undefined` when there is none. */
239
+ export function resolveDocsRoot(env = process.env, from) {
240
+ return resolveDocsCorpus(env, from)?.root;
241
+ }
242
+ function readJson(file) {
243
+ try {
244
+ return JSON.parse(fs.readFileSync(file, "utf8"));
245
+ }
246
+ catch {
247
+ return undefined;
248
+ }
249
+ }
250
+ function channelOf(version) {
251
+ const prerelease = version.split("-", 2)[1];
252
+ return prerelease ? prerelease.split(".", 1)[0] : "stable";
253
+ }
254
+ /**
255
+ * The release the corpus documents, from the same authoritative sources as
256
+ * `scripts/docs-versions.mjs`. Prefers the generated `docs/versions.json` and
257
+ * falls back to `package.json` + `.release-please-manifest.json`.
258
+ */
259
+ export function resolveDocsVersion(root) {
260
+ const generated = readJson(path.join(root, "docs", "versions.json"));
261
+ const packageName = typeof generated?.package === "string" ? generated.package : "@honua/sdk-js";
262
+ const citation = (version) => ({
263
+ package: packageName,
264
+ version,
265
+ channel: channelOf(version),
266
+ documentationChannel: "development",
267
+ policy: "docs/documentation-versions.md",
268
+ versionsManifest: `${SITE_URL}versions.json`,
269
+ });
270
+ if (typeof generated?.latestRelease === "string")
271
+ return citation(generated.latestRelease);
272
+ const pkg = readJson(path.join(root, "package.json"));
273
+ const manifest = readJson(path.join(root, ".release-please-manifest.json"));
274
+ const version = typeof pkg?.version === "string" ? pkg.version : undefined;
275
+ const manifestVersion = typeof manifest?.["."] === "string" ? manifest["."] : undefined;
276
+ if (!version || !manifestVersion || version !== manifestVersion)
277
+ return undefined;
278
+ return citation(version);
279
+ }
280
+ function git(root, args) {
281
+ try {
282
+ return execFileSync("git", [...args], {
283
+ cwd: root,
284
+ encoding: "utf8",
285
+ stdio: ["ignore", "pipe", "ignore"],
286
+ }).trim();
287
+ }
288
+ catch {
289
+ return undefined;
290
+ }
291
+ }
292
+ /**
293
+ * The exact commit the searched corpus bytes come from, or `undefined` when it
294
+ * cannot be established.
295
+ *
296
+ * `docs/documentation-versions.md` is explicit that a matching version string
297
+ * is not proof that development documentation describes the npm artifact, and
298
+ * that every built page identifies its 40-character source commit. A citation
299
+ * pinned to `trunk` breaks that: `trunk` moves, and the text a reader is sent
300
+ * to can stop being the text that was searched. So citations are pinned to the
301
+ * commit instead, using the same `HONUA_SOURCE_REVISION` override
302
+ * `scripts/build-docs-site.mjs` honours.
303
+ *
304
+ * The commit is only used when the corpus at `HEAD` is byte-identical to the
305
+ * corpus that was read (`git hash-object` against `HEAD:<path>`). A locally
306
+ * regenerated or edited `llms-full.txt` is not at any commit, and pinning it to
307
+ * `HEAD` would produce a citation that looks exact and is wrong. When nothing
308
+ * can be established — an installed package with no checkout, no `git`, a dirty
309
+ * corpus — the URL stays on `trunk` and the citation reports
310
+ * `sourceRevision: null` so the consumer can see the link is floating.
311
+ */
312
+ export function resolveCorpusRevision(root, corpusPath, env = process.env) {
313
+ const declared = env.HONUA_SOURCE_REVISION?.trim().toLowerCase();
314
+ if (declared && COMMIT_PATTERN.test(declared))
315
+ return declared;
316
+ const top = git(root, ["rev-parse", "--show-toplevel"]);
317
+ if (!top)
318
+ return undefined;
319
+ const head = git(root, ["rev-parse", "HEAD"])?.toLowerCase();
320
+ if (!head || !COMMIT_PATTERN.test(head))
321
+ return undefined;
322
+ const relative = path.relative(top, path.resolve(corpusPath)).split(path.sep).join("/");
323
+ if (relative.startsWith(".."))
324
+ return undefined;
325
+ const committed = git(root, ["rev-parse", `HEAD:${relative}`]);
326
+ const working = git(root, ["hash-object", corpusPath]);
327
+ if (!committed || !working || committed !== working)
328
+ return undefined;
329
+ return head;
330
+ }
331
+ function sourceUrl(file, revision) {
332
+ return `https://github.com/${REPOSITORY}/blob/${revision ?? "trunk"}/${file}`;
333
+ }
334
+ export async function execute(_client, input) {
335
+ const located = resolveDocsCorpus();
336
+ if (!located) {
337
+ return capabilityUnavailable("Honua documentation corpus", `${CORPUS_FILE} was not found in this installation or any parent directory`, "Run `npm run docs:llms` in a honua-sdk-js checkout, or point HONUA_DOCS_CORPUS_PATH at a generated llms-full.txt.");
338
+ }
339
+ const { corpusPath, root } = located;
340
+ const version = resolveDocsVersion(root);
341
+ if (!version) {
342
+ return capabilityUnavailable("Honua documentation versions", "package.json and .release-please-manifest.json disagree or are unavailable, so no release can be cited", "Run `npm run docs:versions:write` in a honua-sdk-js checkout so documentation answers can cite a release.");
343
+ }
344
+ const documents = splitCorpus(fs.readFileSync(corpusPath, "utf8"));
345
+ const revision = resolveCorpusRevision(root, corpusPath);
346
+ const queryTerms = queryTermsOf(input.query);
347
+ const prefix = input.pathPrefix?.toLowerCase();
348
+ const results = documents
349
+ .filter((document) => (prefix ? document.file.toLowerCase().startsWith(prefix) : true))
350
+ .map((document) => ({ document, score: scoreDocument(document, queryTerms) }))
351
+ .filter((entry) => entry.score > 0)
352
+ .sort((a, b) => b.score - a.score || a.document.file.localeCompare(b.document.file))
353
+ .slice(0, input.limit ?? 3)
354
+ .map((entry) => ({
355
+ title: entry.document.title,
356
+ score: entry.score,
357
+ excerpt: excerptFor(entry.document, queryTerms),
358
+ citation: {
359
+ file: entry.document.file,
360
+ url: sourceUrl(entry.document.file, revision),
361
+ // The commit the cited text was read at. `null` means the URL floats on
362
+ // `trunk` and the text there may already differ from what was searched.
363
+ sourceRevision: revision ?? null,
364
+ package: version.package,
365
+ version: version.version,
366
+ channel: version.channel,
367
+ },
368
+ }));
369
+ return jsonText({
370
+ available: true,
371
+ query: input.query,
372
+ release: version,
373
+ corpus: { file: path.basename(corpusPath), documents: documents.length, sourceRevision: revision ?? null },
374
+ results,
375
+ ...(results.length === 0
376
+ ? {
377
+ guidance: "No indexed document matched. Try fewer or more specific terms, or drop pathPrefix. An empty result means the corpus has no matching text, not that the capability is missing.",
378
+ }
379
+ : {}),
380
+ });
381
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@honua/mcp-server",
3
- "version": "0.1.4-beta.0",
3
+ "version": "0.1.9-beta.0",
4
4
  "description": "Platform-free MCP server for geospatial services: expose discovery, query, and analysis over any public ArcGIS/Esri FeatureServer or OGC API endpoint to any MCP client, with no platform lock-in",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Honua",
@@ -23,7 +23,8 @@
23
23
  "type": "module",
24
24
  "bin": {
25
25
  "honua-mcp": "dist/src/index.js",
26
- "honua-mcp-proxy": "dist/src/proxy.js"
26
+ "honua-mcp-proxy": "dist/src/proxy.js",
27
+ "honua-zero-to-map-release": "dist/src/release/zero-to-map-cli.js"
27
28
  },
28
29
  "exports": {
29
30
  ".": {
@@ -53,6 +54,8 @@
53
54
  "certify:standalone-ogc": "npm run build --silent && node dist/src/certification/cli.js --target standalone-ogc",
54
55
  "test:certification:nl-map-control": "vitest run test/certification/nl-map-control.test.ts",
55
56
  "test:certification": "npm run build --silent && vitest run test/certification && node dist/src/certification/cli.js",
57
+ "test:certification:admin": "vitest run test/proxy.test.ts test/certification/admin-parity.test.ts",
58
+ "certify:admin-parity": "npm run build --silent && node dist/src/certification/admin-parity.js",
56
59
  "test:certification:artifact": "npm run build --silent && node dist/src/certification/cli.js --artifact-only",
57
60
  "test:certification:standalone": "npm run build --silent && node dist/src/certification/cli.js --target standalone",
58
61
  "test:certification:standalone-ogc": "npm run build --silent && node dist/src/certification/cli.js --target standalone-ogc",
@@ -64,6 +67,7 @@
64
67
  "leaderboard": "node scripts/render-leaderboard.mjs",
65
68
  "test:eval": "npm run build --silent && vitest run test/eval && node dist/src/eval/cli.js --offline",
66
69
  "test:eval:artifact": "npm run build --silent && node dist/src/eval/cli.js --artifact-only",
70
+ "release:zero-to-map": "npm run build --silent && node dist/src/release/zero-to-map-cli.js",
67
71
  "lint": "biome lint src/ test/",
68
72
  "lint:fix": "biome lint --write src/ test/",
69
73
  "format": "biome format src/ test/",
@@ -83,7 +87,7 @@
83
87
  "fast-uri": "^3.1.3"
84
88
  },
85
89
  "peerDependencies": {
86
- "@honua/sdk-js": "^0.1.4-beta.0"
90
+ "@honua/sdk-js": "^0.1.9-beta.0"
87
91
  },
88
92
  "devDependencies": {
89
93
  "@biomejs/biome": "1.9.4",
@@ -95,6 +99,7 @@
95
99
  },
96
100
  "files": [
97
101
  "dist/src",
102
+ "release/zero-to-map",
98
103
  "certification/geospatial-mcp-schemas",
99
104
  "README.md",
100
105
  "LICENSE"
@@ -0,0 +1,174 @@
1
+ # Zero-to-map 2026.1 release journey
2
+
3
+ This bundle is the executable contract for `honua-release#123` D9.3: install
4
+ Honua, configure and publish deterministic data with AI-accessible admin tools,
5
+ run Buffer through the AI-facing Esri MCP tool, the SDK's Esri GPServer
6
+ compatibility path, and the MCP-native analysis verb, compose distinct map,
7
+ app, and dashboard packages in Studio, save each as an immutable version, read
8
+ that exact version, reopen it as a new draft, record three publication intents,
9
+ and save three new intent-bearing versions. It then stops at the human Console
10
+ gate before verifying the three approved public URLs. Both MCP
11
+ jobs are polled through `honua://jobs/{id}` and joined to their
12
+ `honua://jobs/{id}/results` packages; a queued submission alone cannot pass.
13
+
14
+ The driver reuses `honua admin install` and `honua-mcp-proxy`. It does not ship
15
+ a second installer, admin client, or MCP transport. The installer is the
16
+ canonical credential-issuance boundary: material is handed directly to its
17
+ private environment/MCP configuration files, while its JSON receipt contains
18
+ only the credential ID, requested/effective grants, active status, and a digest
19
+ of the file reference.
20
+
21
+ ## Inspect the contract without changing anything
22
+
23
+ From the `mcp` package directory:
24
+
25
+ ```sh
26
+ npm run release:zero-to-map -- --output zero-to-map-contract-receipt.json
27
+ ```
28
+
29
+ Contract mode never invokes Docker, CLI, MCP, GPServer, Console, or HTTP. Its
30
+ receipt is intentionally `blocked`, with the first live action blocked and all
31
+ later actions skipped. Static issue references remain under `dependencyRefs`;
32
+ they are never reported as runtime blockers on a passed live receipt.
33
+
34
+ ## Configure an MCP client
35
+
36
+ The `configs` directory contains equivalent examples for Claude Desktop,
37
+ Claude Code, and Cursor. Each launches the existing `honua-mcp-proxy` and points
38
+ it at the remote MCP endpoint. Set `HONUA_MCP_AUTH_TOKEN` separately when
39
+ the deployment requires bearer authentication; do not put credentials in the
40
+ journey plan or a release receipt.
41
+ Credential-bearing proxy and live-journey endpoints require HTTPS, except for
42
+ exact loopback HTTP used by the bounded local-Docker profile. User information,
43
+ query parameters, fragments, cross-origin redirects, and any catalog that is not
44
+ exactly the enabled-profile roster fail closed before the first mutation.
45
+
46
+ ## Run a live candidate
47
+
48
+ Prerequisites:
49
+
50
+ - Node.js 20.19 or newer and Docker with Compose
51
+ - the 2026.1 `honua` CLI work from `honua-sdk-js#1370-#1373`
52
+ - a server build exposing the complete default 432-tool roster (47 static plus
53
+ 385 `honua_admin_*` tools), with all eleven secret/session Admin operations
54
+ absent, **and** the `analysis` and `esri-gp` server profiles enabled so the
55
+ three `honua_esri_gp_*` tools and `honua_buffer_features` are advertised
56
+ (441 tools in total), plus the Studio lifecycle tools including
57
+ `honua_studio_save_version`, `honua_studio_get_version`, and
58
+ `honua_studio_reopen_version`
59
+ - the `geoprocessing/Buffer` GPServer task seeded by the deployment
60
+ - these fixtures available to the server at an HTTP(S) base URL
61
+ - a writable checkpoint path; the first pass emits the exact resolved Console
62
+ receipt request after all pre-approval work completes
63
+
64
+ Serve the checked-in fixtures from a URL reachable by the Honua container, for
65
+ example with an existing static-file server. Then run:
66
+
67
+ ```sh
68
+ npm run release:zero-to-map -- \
69
+ --execute --yes \
70
+ --target local-docker \
71
+ --mcp-url http://localhost:8080/mcp \
72
+ --var fixtureBaseUrl=http://host.docker.internal:4173 \
73
+ --var dbSecretReference=env:HONUA_ZERO_TO_MAP_DB_CONNECTION \
74
+ --var dbSecretType=environment \
75
+ --var candidateId=manifest-sha256:<platform-manifest-sha256> \
76
+ --var releaseId=2026.1 \
77
+ --checkpoint ./zero-to-map.checkpoint.json \
78
+ --output ./zero-to-map-live-receipt.json
79
+ ```
80
+
81
+ The first pass exits `2` with `external-receipt-missing`. It has already run
82
+ stages 1-5 exactly once. Give the checkpoint's resolved
83
+ `consoleReceiptRequest` to the Console producer, carry
84
+ `checkpoint.integrity.digest` outside that file, and resume with the same
85
+ command plus:
86
+
87
+ ```sh
88
+ --checkpoint-digest <checkpoint.integrity.digest> \
89
+ --console-receipt ./console-approval.json
90
+ ```
91
+
92
+ The resume atomically claims the checkpoint before any adapter work. A second
93
+ or concurrent claimant fails closed. The checkpoint is bound to the exact plan
94
+ bytes, SDK source SHA (`HONUA_SOURCE_REVISION`), target, MCP endpoint,
95
+ candidate/release, and (for AWS) provisioning receipt; it contains only
96
+ allowlisted captures/evidence, including the non-secret deterministic
97
+ `serviceName` identity and access-credential ID/grants/reference digest required
98
+ by external producers, and never the database password, admin key, token, or
99
+ authorization material. A successful resume
100
+ marks it consumed and binds the final receipt hash.
101
+
102
+ For a deployment already provisioned by the DevOps ECS producer, use
103
+ `--target aws-ecs --provision-receipt <pre-teardown-binding.json>`. The binding
104
+ must match `contracts/aws-ecs-provision-binding.schema.json`, the candidate and
105
+ release variables, and `--mcp-url=<binding.endpoint>/mcp`. Stage 1 then records
106
+ the real image/digest, Terraform, readiness, and a secret-free access receipt
107
+ whose reference digest is verified against the producer's Secrets Manager ARN;
108
+ it does not invoke or pretend to invoke the Docker installer.
109
+
110
+ The catalog is checked in full before the first MCP call. Missing tools or
111
+ input-schema drift block the run without sending that call.
112
+
113
+ The preflight verifies each enabled server profile independently and *derives*
114
+ the expected total from them; it never compares the catalog against a single
115
+ hardcoded number:
116
+
117
+ | Profile | Members | Verified by |
118
+ | --- | --- | --- |
119
+ | `base` | 47 static + 385 `honua_admin_*` = 432 | the generated Admin contract, plus the 11 audited secret/session exclusions proven absent |
120
+ | `analysis` | 6 | `honua_buffer_features` by name; the rest by declared size (the server has not published an `analysis` roster - honua-server#3363) |
121
+ | `esri-gp` | 3 | exactly `honua_esri_gp_list_tasks`, `honua_esri_gp_describe_task`, `honua_esri_gp_execute_task` from `contracts/esri-gp-mcp.v1.json` |
122
+
123
+ The enabled candidate therefore advertises 432 + 6 + 3 = 441 tools. A missing,
124
+ unexpected, duplicate, excluded, truncated, or schema-drifted member fails with
125
+ its own labelled finding naming the journey stage and action that needs the
126
+ tool; a short catalog is reported as a pagination fault rather than a roster
127
+ decision. The pagination safety ceiling in `listAllTools` is deliberately
128
+ separate from every roster number, so reading a larger profile-enabled catalog
129
+ is never mistaken for a hostile server. Default-profile certification
130
+ (`certifyAdminCatalogParity`) continues to require exactly 432.
131
+
132
+ The connection is
133
+ created with `secretReference` + `secretType`; raw database credentials never
134
+ enter MCP arguments, checkpoints, or receipts. One-time-secret and session-bound
135
+ operations (including API-key issuance) are deliberately absent from MCP and
136
+ must be handled by the install/control-plane credential boundary. The journey
137
+ then exercises only secret-safe API-key list and effective-permissions tools to
138
+ verify the provisioned identity and its exact grants. Admin operations
139
+ must return a completed PublishedOperation; queued or approval-required handles
140
+ produce explicit blocked receipts. The driver reads connection/layer IDs only
141
+ from `structuredContent.details.response`, the server's PublishedOperation
142
+ endpoint-response seam.
143
+
144
+ The Studio version tools are also preflighted before the first server mutation.
145
+ Map, app, and dashboard each exercise layer, style, view, widget, and control
146
+ mutations before validation; family-specific shortcuts or embedded lookalikes
147
+ cannot satisfy the release roster.
148
+ The journey does not treat `honua_studio_get_draft` as reopen evidence: every
149
+ family must capture its `itemId`, immutable `versionId`, `contentHash`, and the
150
+ new draft whose `baseVersionId` points back to that exact version. After the
151
+ proposal increments that reopened draft's generation, the journey saves a new
152
+ immutable publication version. Console must create and publish the request for
153
+ that new version, never the earlier pre-intent version.
154
+
155
+ The Console receipt is accepted only when it is `passed` and binds this
156
+ journey/release contract to the exact connection, service, layers, three GP job
157
+ identities, result packages/artifacts, map/app/dashboard draft and immutable
158
+ version identities, all three reopened drafts and intent-bearing publication
159
+ versions, all three Console request/publication/status/public-URL identities,
160
+ execution operations, audit correlations, routes, candidate, and release. Each
161
+ audit operation must equal its family's execution operation, each proposal ID
162
+ must equal its publication request ID, and the app public URL must equal the
163
+ top-level share URL. The Studio proposal actions reject any pre-approval
164
+ publication ID or public URL and must report both `recorded=true` and
165
+ `humanConfirmationRequired=true`. Agent execution never claims the human
166
+ publication step. Stage 7 independently requires identity-preserving HTTPS
167
+ HTTP 200 responses for map, app, and dashboard URLs.
168
+
169
+ ## Evidence status
170
+
171
+ The fixtures, plan, contracts, and simulated end-to-end tests are deterministic.
172
+ They are not a live release recording. Until all deployment dependencies are
173
+ available and an operator supplies a matching Console receipt, the honest
174
+ candidate result is `blocked`/`skipped`, not passed.
@@ -0,0 +1,11 @@
1
+ {
2
+ "mcpServers": {
3
+ "honua": {
4
+ "command": "npx",
5
+ "args": ["-y", "--package", "@honua/mcp-server@0.1.7-beta.0", "honua-mcp-proxy"],
6
+ "env": {
7
+ "HONUA_MCP_REMOTE_URL": "http://localhost:8080/mcp"
8
+ }
9
+ }
10
+ }
11
+ }
@@ -0,0 +1,11 @@
1
+ {
2
+ "mcpServers": {
3
+ "honua": {
4
+ "command": "npx",
5
+ "args": ["-y", "--package", "@honua/mcp-server@0.1.7-beta.0", "honua-mcp-proxy"],
6
+ "env": {
7
+ "HONUA_MCP_REMOTE_URL": "http://localhost:8080/mcp"
8
+ }
9
+ }
10
+ }
11
+ }
@@ -0,0 +1,11 @@
1
+ {
2
+ "mcpServers": {
3
+ "honua": {
4
+ "command": "npx",
5
+ "args": ["-y", "--package", "@honua/mcp-server@0.1.7-beta.0", "honua-mcp-proxy"],
6
+ "env": {
7
+ "HONUA_MCP_REMOTE_URL": "http://localhost:8080/mcp"
8
+ }
9
+ }
10
+ }
11
+ }