@honua/mcp-server 0.0.20-alpha.0 → 0.1.1-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 (107) hide show
  1. package/README.md +141 -15
  2. package/certification/geospatial-mcp-schemas/PROVENANCE.md +15 -7
  3. package/certification/geospatial-mcp-schemas/conformance/manifest.schema.json +6 -0
  4. package/certification/geospatial-mcp-schemas/index.json +88 -9
  5. package/certification/geospatial-mcp-schemas/tools/apply_style_preset.schema.json +13 -9
  6. package/certification/geospatial-mcp-schemas/tools/buffer_features.schema.json +60 -0
  7. package/certification/geospatial-mcp-schemas/tools/edit_features.schema.json +74 -0
  8. package/certification/geospatial-mcp-schemas/tools/export_dataset.schema.json +60 -0
  9. package/certification/geospatial-mcp-schemas/tools/geocode_addresses.schema.json +34 -0
  10. package/certification/geospatial-mcp-schemas/tools/get_style.schema.json +13 -3
  11. package/certification/geospatial-mcp-schemas/tools/ingest_dataset.schema.json +45 -0
  12. package/certification/geospatial-mcp-schemas/tools/join_features.schema.json +73 -0
  13. package/certification/geospatial-mcp-schemas/tools/overlay_features.schema.json +53 -0
  14. package/certification/geospatial-mcp-schemas/tools/query_features.schema.json +20 -0
  15. package/certification/geospatial-mcp-schemas/tools/render_map.schema.json +5 -0
  16. package/certification/geospatial-mcp-schemas/tools/reproject_features.schema.json +49 -0
  17. package/certification/geospatial-mcp-schemas/tools/summarize_statistics.schema.json +71 -0
  18. package/dist/src/agent-execution.d.ts +16 -0
  19. package/dist/src/agent-execution.d.ts.map +1 -0
  20. package/dist/src/agent-execution.js +46 -0
  21. package/dist/src/capability.d.ts +48 -0
  22. package/dist/src/capability.d.ts.map +1 -0
  23. package/dist/src/capability.js +44 -0
  24. package/dist/src/certification/census-data.d.ts +33 -0
  25. package/dist/src/certification/census-data.d.ts.map +1 -0
  26. package/dist/src/certification/census-data.js +506 -0
  27. package/dist/src/certification/census-fixture-client.d.ts +7 -0
  28. package/dist/src/certification/census-fixture-client.d.ts.map +1 -0
  29. package/dist/src/certification/census-fixture-client.js +226 -0
  30. package/dist/src/certification/certifier.d.ts +7 -1
  31. package/dist/src/certification/certifier.d.ts.map +1 -1
  32. package/dist/src/certification/certifier.js +52 -7
  33. package/dist/src/certification/cli.js +19 -4
  34. package/dist/src/certification/contracts.d.ts +41 -0
  35. package/dist/src/certification/contracts.d.ts.map +1 -1
  36. package/dist/src/certification/contracts.js +71 -0
  37. package/dist/src/certification/fixture-client.d.ts.map +1 -1
  38. package/dist/src/certification/fixtures.d.ts +13 -0
  39. package/dist/src/certification/fixtures.d.ts.map +1 -1
  40. package/dist/src/certification/fixtures.js +28 -0
  41. package/dist/src/certification/json-schema.d.ts +23 -1
  42. package/dist/src/certification/json-schema.d.ts.map +1 -1
  43. package/dist/src/certification/json-schema.js +10 -2
  44. package/dist/src/certification/lifecycle.d.ts +14 -0
  45. package/dist/src/certification/lifecycle.d.ts.map +1 -0
  46. package/dist/src/certification/lifecycle.js +326 -0
  47. package/dist/src/certification/operator-catalog.d.ts +8 -0
  48. package/dist/src/certification/operator-catalog.d.ts.map +1 -1
  49. package/dist/src/certification/operator-catalog.js +189 -5
  50. package/dist/src/certification/report.d.ts.map +1 -1
  51. package/dist/src/certification/report.js +13 -0
  52. package/dist/src/certification/run.d.ts.map +1 -1
  53. package/dist/src/certification/run.js +1 -0
  54. package/dist/src/certification/schema-index.d.ts +25 -0
  55. package/dist/src/certification/schema-index.d.ts.map +1 -1
  56. package/dist/src/certification/schema-index.js +27 -0
  57. package/dist/src/certification/target.d.ts +9 -1
  58. package/dist/src/certification/target.d.ts.map +1 -1
  59. package/dist/src/certification/target.js +54 -2
  60. package/dist/src/eval/cli.js +4 -2
  61. package/dist/src/eval/corpus.d.ts +7 -0
  62. package/dist/src/eval/corpus.d.ts.map +1 -1
  63. package/dist/src/eval/corpus.js +16 -0
  64. package/dist/src/eval/drivers/deterministic.d.ts.map +1 -1
  65. package/dist/src/eval/drivers/deterministic.js +13 -0
  66. package/dist/src/eval/grade.d.ts.map +1 -1
  67. package/dist/src/eval/grade.js +20 -1
  68. package/dist/src/eval/report.d.ts +5 -0
  69. package/dist/src/eval/report.d.ts.map +1 -1
  70. package/dist/src/eval/report.js +3 -0
  71. package/dist/src/eval/runner.d.ts +6 -0
  72. package/dist/src/eval/runner.d.ts.map +1 -1
  73. package/dist/src/eval/runner.js +21 -7
  74. package/dist/src/eval/standalone-corpus.d.ts +3 -0
  75. package/dist/src/eval/standalone-corpus.d.ts.map +1 -0
  76. package/dist/src/eval/standalone-corpus.js +699 -0
  77. package/dist/src/eval/types.d.ts +25 -0
  78. package/dist/src/eval/types.d.ts.map +1 -1
  79. package/dist/src/helpers.d.ts +1 -1
  80. package/dist/src/helpers.d.ts.map +1 -1
  81. package/dist/src/index.d.ts +37 -6
  82. package/dist/src/index.d.ts.map +1 -1
  83. package/dist/src/index.js +47 -22
  84. package/dist/src/nl-map-control.d.ts +70 -0
  85. package/dist/src/nl-map-control.d.ts.map +1 -0
  86. package/dist/src/nl-map-control.js +470 -0
  87. package/dist/src/provenance.d.ts +44 -0
  88. package/dist/src/provenance.d.ts.map +1 -0
  89. package/dist/src/provenance.js +37 -0
  90. package/dist/src/resources/styles.d.ts.map +1 -1
  91. package/dist/src/resources/styles.js +36 -3
  92. package/dist/src/stable-json.d.ts +14 -0
  93. package/dist/src/stable-json.d.ts.map +1 -0
  94. package/dist/src/stable-json.js +126 -0
  95. package/dist/src/styles.d.ts +2 -0
  96. package/dist/src/styles.d.ts.map +1 -1
  97. package/dist/src/styles.js +29 -3
  98. package/dist/src/tools/apply-style-preset.d.ts.map +1 -1
  99. package/dist/src/tools/apply-style-preset.js +14 -2
  100. package/dist/src/tools/explain-capability-gap.d.ts +3 -3
  101. package/dist/src/tools/explain-capability-gap.d.ts.map +1 -1
  102. package/dist/src/tools/explain-capability-gap.js +1 -1
  103. package/dist/src/tools/get-style.d.ts.map +1 -1
  104. package/dist/src/tools/get-style.js +24 -13
  105. package/dist/src/tools/list-services.d.ts.map +1 -1
  106. package/dist/src/tools/list-services.js +16 -1
  107. package/package.json +27 -2
@@ -0,0 +1,470 @@
1
+ import { verifyAgentApproval, verifyAgentStepAuthorization, } from "@honua/sdk-js/agent-safety";
2
+ import { NL_MAP_CONTROL_TOOL_DEFINITIONS, NL_MAP_CONTROL_VERSION, NL_MAP_PLAN_KIND, agentEffectForNlEffect, createNlMapControl, hashNlMapPlan, } from "@honua/sdk-js/nl-map-control";
3
+ import { canonicalStringify, sha256, toJsonValue } from "@honua/sdk-js/query-planner";
4
+ import { z } from "zod";
5
+ import { snapshotMcpJson } from "./stable-json.js";
6
+ const MCP_NL_MAP_RECEIPT_KIND = "honua.mcp-nl-map-receipt";
7
+ const PLAN_ID = /^nlplan_[0-9a-f]{16}$/;
8
+ const DIGEST = /^sha256:[0-9a-f]{64}$/;
9
+ const SENSITIVE_KEYS = new Set([
10
+ "accesstoken",
11
+ "apikey",
12
+ "authorization",
13
+ "cookie",
14
+ "credential",
15
+ "credentials",
16
+ "cursor",
17
+ "endpoint",
18
+ "password",
19
+ "refreshtoken",
20
+ "resumetoken",
21
+ "secret",
22
+ "signedurl",
23
+ "token",
24
+ "url",
25
+ "uri",
26
+ "watermark",
27
+ ]);
28
+ const CREDENTIAL_TEXT = /\b(?:bearer|basic)\s+[A-Za-z0-9._~+/=-]+|\b(?:api[_-]?key|access[_-]?token|password|secret)\s*[:=]\s*[^\s,;]+/giu;
29
+ const CURSOR_TEXT = /\b(?:cursor|resume[_ -]?token|watermark)\s*[:=]\s*[^\s,;]+/giu;
30
+ const URL_TEXT = /https?:\/\/[^\s"'<>]+/giu;
31
+ const PUBLIC_ERROR_CODES = new Set([
32
+ "aborted",
33
+ "approval-expired",
34
+ "approval-invalid",
35
+ "approval-required",
36
+ "authorization-scope-denied",
37
+ "cancelled",
38
+ "context-mismatch",
39
+ "fixture-mismatch",
40
+ "integrity-failed",
41
+ "invalid-input",
42
+ "invalid-options",
43
+ "plan-invalid",
44
+ "plan-required",
45
+ "policy-denied",
46
+ "refusal",
47
+ "retries-exhausted",
48
+ "signature-invalid",
49
+ "unsafe-output",
50
+ ]);
51
+ class HonuaNlMapMcpError extends Error {
52
+ code;
53
+ constructor(code, message) {
54
+ super(message);
55
+ this.name = "HonuaNlMapMcpError";
56
+ this.code = code;
57
+ }
58
+ }
59
+ /**
60
+ * Bind the experimental NL map controller to the MCP trust boundary.
61
+ *
62
+ * The host pre-authorizes and atomically consumes every approved step before
63
+ * delegating to `createNlMapControl`. This preserves the SDK's exact plan and
64
+ * signature checks while preventing approval replay through a second transport
65
+ * call. No caller-provided credential or cursor is included in MCP output.
66
+ */
67
+ export function createNlMapControlMcpHost(options) {
68
+ if (!options?.control?.approvalVerifier) {
69
+ throw new TypeError("NL map MCP hosting requires control.approvalVerifier");
70
+ }
71
+ if (!options.approvalUseConsumer) {
72
+ throw new TypeError("NL map MCP hosting requires an atomic approvalUseConsumer");
73
+ }
74
+ const control = createNlMapControl(options.control);
75
+ const approvalVerifier = options.control.approvalVerifier;
76
+ const approvalUseConsumer = options.approvalUseConsumer;
77
+ const resolveAuthorizationScopes = options.resolveAuthorizationScopes;
78
+ const configuredNow = options.control.policy?.now;
79
+ const now = () => configuredNow?.() ?? new Date().toISOString();
80
+ return Object.freeze({
81
+ control,
82
+ async propose(input, context = {}) {
83
+ const signal = requestSignal(context);
84
+ signal?.throwIfAborted();
85
+ const request = snapshotMcpObject(input, "proposeMapPlan input");
86
+ const instruction = request.instruction;
87
+ if (typeof instruction !== "string" || instruction.trim().length === 0 || instruction.length > 16_384) {
88
+ throw new HonuaNlMapMcpError("invalid-input", "proposeMapPlan requires a non-empty instruction");
89
+ }
90
+ const plan = parseMcpPlan(await control.propose(instruction, {
91
+ ...(signal ? { signal } : {}),
92
+ }));
93
+ signal?.throwIfAborted();
94
+ assertMcpSafePlan(plan);
95
+ return Object.freeze({ plan, approvalRequired: planRequiresMcpApproval(plan) });
96
+ },
97
+ async execute(input, context = {}) {
98
+ const signal = requestSignal(context);
99
+ signal?.throwIfAborted();
100
+ const request = snapshotMcpObject(input, "executeMapPlan input");
101
+ const plan = parseMcpPlan(request.plan);
102
+ assertMcpSafePlan(plan);
103
+ const approval = request.approval === undefined ? undefined : request.approval;
104
+ if (approval === undefined && planRequiresMcpApproval(plan)) {
105
+ throw new HonuaNlMapMcpError("approval-required", "MCP source-scoped and effectful map plans require a signed approval envelope");
106
+ }
107
+ if (approval !== undefined) {
108
+ const authorizationTime = now();
109
+ await verifyAgentApproval(approval.dryRun, approval.policy, approval.approval, approvalVerifier, approval.context, {
110
+ now: authorizationTime,
111
+ ...(signal ? { signal } : {}),
112
+ });
113
+ signal?.throwIfAborted();
114
+ assertApprovalPlanIdentity(plan, approval);
115
+ const transportScopes = requestAuthorizationScopes(context);
116
+ const grantedInput = resolveAuthorizationScopes
117
+ ? await resolveAuthorizationScopes(transportScopes)
118
+ : transportScopes;
119
+ signal?.throwIfAborted();
120
+ const grantedScopes = snapshotAuthorizationScopes(grantedInput, "resolved authorization scopes");
121
+ assertAuthorizationScopes(approval, grantedScopes);
122
+ for (let index = 0; index < plan.steps.length; index += 1) {
123
+ signal?.throwIfAborted();
124
+ const step = plan.steps[index];
125
+ const approvedStep = approval.dryRun.plan.steps[index];
126
+ const sourceId = authorizationSourceId(step, approvedStep.source.id);
127
+ await verifyAgentStepAuthorization(approval.dryRun, approval.policy, approval.approval, approvalVerifier, approval.context, step.id, {
128
+ tool: step.tool,
129
+ effect: agentEffectForNlEffect(step.effect),
130
+ sourceId,
131
+ queryPlan: approvedStep.queryPlan,
132
+ fields: approvedStep.fields,
133
+ parameters: toJsonValue(step.call.args ?? {}),
134
+ }, approvalUseConsumer, {
135
+ now: authorizationTime,
136
+ ...(signal ? { signal } : {}),
137
+ });
138
+ }
139
+ }
140
+ signal?.throwIfAborted();
141
+ const execution = await control.execute(plan, {
142
+ ...(approval ? { approval } : {}),
143
+ ...(signal ? { signal } : {}),
144
+ });
145
+ return executionResponse(execution);
146
+ },
147
+ });
148
+ }
149
+ const proposeSchema = z
150
+ .object({
151
+ instruction: z.string().trim().min(1).max(16_384),
152
+ })
153
+ .strict();
154
+ const executeSchema = z
155
+ .object({
156
+ plan: z.record(z.string(), z.unknown()),
157
+ approval: z.record(z.string(), z.unknown()).optional(),
158
+ })
159
+ .strict();
160
+ /** Register `proposeMapPlan` and `executeMapPlan` on a real MCP server. */
161
+ export function registerNlMapControlMcpTools(server, host) {
162
+ const proposeDefinition = NL_MAP_CONTROL_TOOL_DEFINITIONS[0];
163
+ const executeDefinition = NL_MAP_CONTROL_TOOL_DEFINITIONS[1];
164
+ server.registerTool(proposeDefinition.name, {
165
+ title: proposeDefinition.title,
166
+ description: proposeDefinition.description,
167
+ inputSchema: proposeSchema,
168
+ annotations: {
169
+ readOnlyHint: true,
170
+ destructiveHint: false,
171
+ idempotentHint: false,
172
+ openWorldHint: true,
173
+ },
174
+ }, async (args, extra) => {
175
+ try {
176
+ return successResult(await host.propose(args, {
177
+ signal: extra.signal,
178
+ transportAuthorizationScopes: extra.authInfo?.scopes ?? [],
179
+ }));
180
+ }
181
+ catch (error) {
182
+ return errorResult(error, extra.signal);
183
+ }
184
+ });
185
+ server.registerTool(executeDefinition.name, {
186
+ title: executeDefinition.title,
187
+ description: `${executeDefinition.description} In the MCP host, source-scoped reads also require a signed approval so transport scopes can be matched to the exact source binding.`,
188
+ inputSchema: executeSchema,
189
+ annotations: {
190
+ readOnlyHint: false,
191
+ destructiveHint: true,
192
+ idempotentHint: false,
193
+ openWorldHint: false,
194
+ },
195
+ }, async (args, extra) => {
196
+ try {
197
+ return successResult(await host.execute({ plan: args.plan, ...(args.approval === undefined ? {} : { approval: args.approval }) }, {
198
+ signal: extra.signal,
199
+ transportAuthorizationScopes: extra.authInfo?.scopes ?? [],
200
+ }));
201
+ }
202
+ catch (error) {
203
+ return errorResult(error, extra.signal);
204
+ }
205
+ });
206
+ }
207
+ function snapshotMcpObject(input, label, code = "invalid-input") {
208
+ try {
209
+ const snapshot = snapshotMcpJson(input, label);
210
+ if (snapshot === null || typeof snapshot !== "object" || Array.isArray(snapshot)) {
211
+ throw new TypeError(`${label} must be an object`);
212
+ }
213
+ return snapshot;
214
+ }
215
+ catch {
216
+ throw new HonuaNlMapMcpError(code, `${label} is not safe bounded JSON`);
217
+ }
218
+ }
219
+ function requestSignal(context) {
220
+ const value = requestContextProperty(context, "signal");
221
+ if (value === undefined)
222
+ return undefined;
223
+ try {
224
+ if (!(value instanceof AbortSignal))
225
+ throw new TypeError("signal must be an AbortSignal");
226
+ }
227
+ catch {
228
+ throw new HonuaNlMapMcpError("invalid-input", "MCP request context contains an invalid signal");
229
+ }
230
+ return value;
231
+ }
232
+ function requestAuthorizationScopes(context) {
233
+ const value = requestContextProperty(context, "transportAuthorizationScopes");
234
+ return snapshotAuthorizationScopes(value ?? [], "transport authorization scopes");
235
+ }
236
+ function requestContextProperty(context, key) {
237
+ try {
238
+ if (!context || typeof context !== "object" || Array.isArray(context)) {
239
+ throw new TypeError("context must be an object");
240
+ }
241
+ const prototype = Object.getPrototypeOf(context);
242
+ if (prototype !== Object.prototype && prototype !== null)
243
+ throw new TypeError("context must be plain");
244
+ const descriptor = Reflect.getOwnPropertyDescriptor(context, key);
245
+ if (!descriptor)
246
+ return undefined;
247
+ if (!("value" in descriptor) || !descriptor.enumerable)
248
+ throw new TypeError("context property must be data");
249
+ return descriptor.value;
250
+ }
251
+ catch {
252
+ throw new HonuaNlMapMcpError("invalid-input", "MCP request context could not be read safely");
253
+ }
254
+ }
255
+ function snapshotAuthorizationScopes(input, label) {
256
+ let snapshot;
257
+ try {
258
+ snapshot = snapshotMcpJson(input, label);
259
+ }
260
+ catch {
261
+ throw new HonuaNlMapMcpError("invalid-input", `${label} are not safe bounded JSON`);
262
+ }
263
+ if (!Array.isArray(snapshot) ||
264
+ snapshot.length > 128 ||
265
+ snapshot.some((scope) => typeof scope !== "string" || scope.length === 0 || hasControlCharacters(scope))) {
266
+ throw new HonuaNlMapMcpError("invalid-input", `${label} must be a bounded list of non-empty strings`);
267
+ }
268
+ return Object.freeze([...new Set(snapshot)].sort());
269
+ }
270
+ function parseMcpPlan(input) {
271
+ const plan = snapshotMcpObject(input, "NL map plan");
272
+ if (plan.kind !== NL_MAP_PLAN_KIND ||
273
+ plan.version !== NL_MAP_CONTROL_VERSION ||
274
+ typeof plan.id !== "string" ||
275
+ !PLAN_ID.test(plan.id) ||
276
+ typeof plan.fingerprint !== "string" ||
277
+ !DIGEST.test(plan.fingerprint) ||
278
+ !Array.isArray(plan.steps) ||
279
+ plan.steps.length === 0) {
280
+ throw new HonuaNlMapMcpError("invalid-input", "executeMapPlan requires a valid NL map plan identity");
281
+ }
282
+ let fingerprint;
283
+ try {
284
+ fingerprint = hashNlMapPlan(plan);
285
+ }
286
+ catch {
287
+ throw new HonuaNlMapMcpError("invalid-input", "executeMapPlan plan content is not canonical JSON");
288
+ }
289
+ const expectedId = `nlplan_${fingerprint.slice("sha256:".length, "sha256:".length + 16)}`;
290
+ if (fingerprint !== plan.fingerprint || plan.id !== expectedId) {
291
+ throw new HonuaNlMapMcpError("invalid-input", "executeMapPlan plan identity does not match its content");
292
+ }
293
+ return plan;
294
+ }
295
+ function assertApprovalPlanIdentity(plan, approval) {
296
+ if (!approval ||
297
+ typeof approval !== "object" ||
298
+ !approval.dryRun ||
299
+ approval.dryRun.plan.id !== plan.id ||
300
+ approval.dryRun.plan.steps.length !== plan.steps.length) {
301
+ throw new HonuaNlMapMcpError("invalid-input", "approval does not bind to the supplied plan identity");
302
+ }
303
+ for (let index = 0; index < plan.steps.length; index += 1) {
304
+ const step = plan.steps[index];
305
+ const approved = approval.dryRun.plan.steps[index];
306
+ if (approved.id !== step.id ||
307
+ approved.tool !== step.tool ||
308
+ approved.effect !== agentEffectForNlEffect(step.effect) ||
309
+ approved.queryPlan.fingerprint !== plan.fingerprint) {
310
+ throw new HonuaNlMapMcpError("invalid-input", "approval step identity does not match the supplied plan");
311
+ }
312
+ authorizationSourceId(step, approved.source.id);
313
+ }
314
+ }
315
+ function authorizationSourceId(step, approvedSourceId) {
316
+ const sourceId = step.call.args?.sourceId;
317
+ if (sourceId !== undefined) {
318
+ if (typeof sourceId !== "string" || sourceId.length === 0 || sourceId !== approvedSourceId) {
319
+ throw new HonuaNlMapMcpError("invalid-input", "approval source binding does not match the supplied map operation");
320
+ }
321
+ return sourceId;
322
+ }
323
+ return approvedSourceId;
324
+ }
325
+ function planRequiresMcpApproval(plan) {
326
+ return (!plan.readOnly ||
327
+ plan.steps.some((step) => step.call.args?.sourceId !== undefined));
328
+ }
329
+ function assertAuthorizationScopes(approval, grantedInput) {
330
+ const granted = new Set(grantedInput);
331
+ const required = new Set(approval.dryRun.plan.steps.flatMap((step) => [...step.source.authorizationScope]));
332
+ for (const scope of required) {
333
+ if (!granted.has(scope)) {
334
+ throw new HonuaNlMapMcpError("authorization-scope-denied", "MCP transport authorization does not cover the approved map operation");
335
+ }
336
+ }
337
+ }
338
+ function assertMcpSafePlan(plan) {
339
+ assertMcpSafeJson(plan, "NL map plan");
340
+ }
341
+ function assertMcpSafeJson(value, label) {
342
+ let original;
343
+ let redacted;
344
+ try {
345
+ original = canonicalStringify(toJsonValue(value));
346
+ redacted = canonicalStringify(toJsonValue(redactForMcp(value)));
347
+ }
348
+ catch {
349
+ throw new HonuaNlMapMcpError("invalid-input", `${label} is not safe canonical JSON`);
350
+ }
351
+ if (original !== redacted) {
352
+ throw new HonuaNlMapMcpError("unsafe-output", `${label} contains credential, cursor, or endpoint material that cannot cross the MCP boundary`);
353
+ }
354
+ }
355
+ function executionResponse(execution) {
356
+ const sdkReceipt = execution.receipt;
357
+ const unsignedReceipt = snapshotMcpObject({
358
+ kind: MCP_NL_MAP_RECEIPT_KIND,
359
+ version: NL_MAP_CONTROL_VERSION,
360
+ planId: execution.planId,
361
+ planFingerprint: execution.planFingerprint,
362
+ mode: execution.mode,
363
+ outcome: execution.outcome,
364
+ ...(sdkReceipt.approvalDigest ? { approvalDigest: sdkReceipt.approvalDigest } : {}),
365
+ startedAt: sdkReceipt.startedAt,
366
+ completedAt: sdkReceipt.completedAt,
367
+ steps: sdkReceipt.steps,
368
+ sdkReceiptDigest: sdkReceipt.receiptDigest,
369
+ }, "MCP execution receipt", "unsafe-output");
370
+ assertMcpSafeJson(unsignedReceipt, "MCP execution receipt");
371
+ const receiptDigest = sha256(canonicalStringify(toJsonValue(unsignedReceipt)));
372
+ const receipt = snapshotMcpObject({
373
+ ...unsignedReceipt,
374
+ id: `mcpreceipt_${receiptDigest.slice("sha256:".length, "sha256:".length + 16)}`,
375
+ receiptDigest,
376
+ }, "MCP execution receipt", "unsafe-output");
377
+ const response = snapshotMcpObject({
378
+ planId: execution.planId,
379
+ planFingerprint: execution.planFingerprint,
380
+ mode: execution.mode,
381
+ outcome: execution.outcome,
382
+ receipt,
383
+ }, "MCP execution response", "unsafe-output");
384
+ assertMcpSafeJson(response, "MCP execution response");
385
+ return response;
386
+ }
387
+ function successResult(value) {
388
+ return {
389
+ content: [{ type: "text", text: JSON.stringify(value) }],
390
+ };
391
+ }
392
+ function errorResult(error, signal) {
393
+ const code = signal.aborted ? "cancelled" : safeErrorCode(error);
394
+ const message = safeErrorMessage(code);
395
+ return {
396
+ isError: true,
397
+ content: [{ type: "text", text: JSON.stringify({ error: { code, message } }) }],
398
+ };
399
+ }
400
+ function safeErrorCode(error) {
401
+ try {
402
+ if (error && typeof error === "object") {
403
+ const descriptor = Reflect.getOwnPropertyDescriptor(error, "code");
404
+ const code = descriptor && "value" in descriptor ? descriptor.value : undefined;
405
+ if (typeof code === "string" && PUBLIC_ERROR_CODES.has(code))
406
+ return code;
407
+ }
408
+ }
409
+ catch {
410
+ return "execution-refused";
411
+ }
412
+ return "execution-refused";
413
+ }
414
+ function safeErrorMessage(code) {
415
+ switch (code) {
416
+ case "cancelled":
417
+ case "aborted":
418
+ return "The map-plan request was cancelled before execution.";
419
+ case "authorization-scope-denied":
420
+ return "The authenticated MCP scopes do not authorize this map plan.";
421
+ case "approval-required":
422
+ return "This map plan requires a signed approval envelope.";
423
+ case "approval-expired":
424
+ return "The map-plan approval has expired.";
425
+ case "policy-denied":
426
+ return "The map-plan approval was denied or has already been used.";
427
+ case "signature-invalid":
428
+ case "integrity-failed":
429
+ case "context-mismatch":
430
+ case "approval-invalid":
431
+ return "The map-plan approval could not be verified.";
432
+ case "plan-invalid":
433
+ case "plan-required":
434
+ case "invalid-input":
435
+ return "The supplied map plan or approval is invalid.";
436
+ case "unsafe-output":
437
+ return "The plan was refused because its output would expose sensitive material.";
438
+ default:
439
+ return "The map-plan request was refused safely.";
440
+ }
441
+ }
442
+ function redactForMcp(value, key) {
443
+ if (key && SENSITIVE_KEYS.has(normalizeKey(key)))
444
+ return "[REDACTED]";
445
+ if (typeof value === "string")
446
+ return redactText(value);
447
+ if (Array.isArray(value))
448
+ return value.map((item) => redactForMcp(item));
449
+ if (value && typeof value === "object") {
450
+ return Object.fromEntries(Object.entries(value).map(([name, child]) => [name, redactForMcp(child, name)]));
451
+ }
452
+ return value;
453
+ }
454
+ function normalizeKey(value) {
455
+ return value.toLowerCase().replace(/[^a-z0-9]/gu, "");
456
+ }
457
+ function hasControlCharacters(value) {
458
+ for (let index = 0; index < value.length; index += 1) {
459
+ const code = value.charCodeAt(index);
460
+ if (code <= 31 || code === 127)
461
+ return true;
462
+ }
463
+ return false;
464
+ }
465
+ function redactText(value) {
466
+ return value
467
+ .replace(URL_TEXT, "[REDACTED_URL]")
468
+ .replace(CREDENTIAL_TEXT, "[REDACTED]")
469
+ .replace(CURSOR_TEXT, "[REDACTED]");
470
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Self-proving provenance for certification & eval artifacts (A5 provability).
3
+ *
4
+ * A published report is only evidence if a reader can verify WHAT was tested and
5
+ * WITH WHICH code. Every artifact carries a `provenance` block naming the target
6
+ * surface, the negotiated MCP protocol version, the advertised tool count, the
7
+ * auth mode, the git SHA of the certification/eval suite, and a timestamp. That
8
+ * turns "we pass" into "here is the exact, reproducible run that passed".
9
+ */
10
+ /** How the eval/cert authenticated to the surface (proves the run was authed). */
11
+ export type AuthMode = "bearer" | "api-key" | "anonymous" | "none" | "unknown";
12
+ /** The reproducibility fingerprint attached to every artifact. */
13
+ export interface SuiteProvenance {
14
+ /** Git SHA of the certification/eval suite that produced this artifact. */
15
+ suiteGitSha: string;
16
+ /** Where the SHA came from (CI env, local git, or unresolved). */
17
+ suiteGitShaSource: "env" | "git" | "unknown";
18
+ /** Human-readable target surface / URL under test. */
19
+ targetUrl: string;
20
+ /** MCP protocol version negotiated with the surface, when observable. */
21
+ protocolVersion: string | null;
22
+ /** Number of tools the surface advertised. */
23
+ toolCount: number;
24
+ /** How the run authenticated to the surface. */
25
+ authMode: AuthMode;
26
+ }
27
+ /**
28
+ * Resolve the git SHA of the running suite. Prefers the CI-provided `GITHUB_SHA`
29
+ * (or an explicit `HONUA_SUITE_GIT_SHA` override), then falls back to
30
+ * `git rev-parse HEAD`, then to `"unknown"` so an artifact is always self-labeled
31
+ * even outside a git checkout.
32
+ */
33
+ export declare function resolveSuiteGitSha(env?: NodeJS.ProcessEnv): {
34
+ sha: string;
35
+ source: SuiteProvenance["suiteGitShaSource"];
36
+ };
37
+ /**
38
+ * Read the negotiated MCP protocol version off a connected client, if its
39
+ * transport exposes one (the streamable-HTTP client transport does after
40
+ * `initialize`). Returns `null` for transports that do not surface it
41
+ * (stdio / in-memory), so the field is honest rather than fabricated.
42
+ */
43
+ export declare function readNegotiatedProtocolVersion(client: unknown): string | null;
44
+ //# sourceMappingURL=provenance.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"provenance.d.ts","sourceRoot":"","sources":["../../src/provenance.ts"],"names":[],"mappings":"AAEA;;;;;;;;GAQG;AAEH,kFAAkF;AAClF,MAAM,MAAM,QAAQ,GAAG,QAAQ,GAAG,SAAS,GAAG,WAAW,GAAG,MAAM,GAAG,SAAS,CAAC;AAE/E,kEAAkE;AAClE,MAAM,WAAW,eAAe;IAC9B,2EAA2E;IAC3E,WAAW,EAAE,MAAM,CAAC;IACpB,kEAAkE;IAClE,iBAAiB,EAAE,KAAK,GAAG,KAAK,GAAG,SAAS,CAAC;IAC7C,sDAAsD;IACtD,SAAS,EAAE,MAAM,CAAC;IAClB,yEAAyE;IACzE,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,8CAA8C;IAC9C,SAAS,EAAE,MAAM,CAAC;IAClB,gDAAgD;IAChD,QAAQ,EAAE,QAAQ,CAAC;CACpB;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG;IACxE,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,eAAe,CAAC,mBAAmB,CAAC,CAAC;CAC9C,CAiBA;AAED;;;;;GAKG;AACH,wBAAgB,6BAA6B,CAAC,MAAM,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAI5E"}
@@ -0,0 +1,37 @@
1
+ import { execFileSync } from "node:child_process";
2
+ /**
3
+ * Resolve the git SHA of the running suite. Prefers the CI-provided `GITHUB_SHA`
4
+ * (or an explicit `HONUA_SUITE_GIT_SHA` override), then falls back to
5
+ * `git rev-parse HEAD`, then to `"unknown"` so an artifact is always self-labeled
6
+ * even outside a git checkout.
7
+ */
8
+ export function resolveSuiteGitSha(env = process.env) {
9
+ const fromEnv = (env.HONUA_SUITE_GIT_SHA ?? env.GITHUB_SHA ?? "").trim();
10
+ if (fromEnv.length > 0) {
11
+ return { sha: fromEnv, source: "env" };
12
+ }
13
+ try {
14
+ const sha = execFileSync("git", ["rev-parse", "HEAD"], {
15
+ encoding: "utf8",
16
+ stdio: ["ignore", "pipe", "ignore"],
17
+ }).trim();
18
+ if (sha.length > 0) {
19
+ return { sha, source: "git" };
20
+ }
21
+ }
22
+ catch {
23
+ // not a git checkout / git unavailable
24
+ }
25
+ return { sha: "unknown", source: "unknown" };
26
+ }
27
+ /**
28
+ * Read the negotiated MCP protocol version off a connected client, if its
29
+ * transport exposes one (the streamable-HTTP client transport does after
30
+ * `initialize`). Returns `null` for transports that do not surface it
31
+ * (stdio / in-memory), so the field is honest rather than fabricated.
32
+ */
33
+ export function readNegotiatedProtocolVersion(client) {
34
+ const transport = client.transport;
35
+ const version = transport?.protocolVersion;
36
+ return typeof version === "string" && version.length > 0 ? version : null;
37
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"styles.d.ts","sourceRoot":"","sources":["../../../src/resources/styles.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAGjD,8DAA8D;AAC9D,eAAO,MAAM,GAAG,mBAAmB,CAAC;AAEpC,6EAA6E;AAC7E,eAAO,MAAM,WAAW,6BAA6B,CAAC;AAEtD;;;GAGG;AACH,wBAAsB,WAAW,CAAC,MAAM,EAAE,WAAW;;;;;;GAiBpD;AAED;;;;GAIG;AACH,wBAAsB,IAAI,CAAC,MAAM,EAAE,WAAW,EAAE,OAAO,EAAE,MAAM;;;;;;GAY9D"}
1
+ {"version":3,"file":"styles.d.ts","sourceRoot":"","sources":["../../../src/resources/styles.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAQjD,8DAA8D;AAC9D,eAAO,MAAM,GAAG,mBAAmB,CAAC;AAEpC,6EAA6E;AAC7E,eAAO,MAAM,WAAW,6BAA6B,CAAC;AAEtD;;;GAGG;AACH,wBAAsB,WAAW,CAAC,MAAM,EAAE,WAAW;;;;;;GAyBpD;AAeD;;;;GAIG;AACH,wBAAsB,IAAI,CAAC,MAAM,EAAE,WAAW,EAAE,OAAO,EAAE,MAAM;;;;;;GAoB9D"}
@@ -1,4 +1,7 @@
1
- import { listStyles, resolveStyleRef } from "../styles.js";
1
+ import { CapabilityUnavailableError, capabilityUnavailablePayload } from "../capability.js";
2
+ import { STYLE_SURFACE, listStyles, resolveStyleRef } from "../styles.js";
3
+ const STYLE_GUIDANCE = "This target is a plain FeatureServer/OGC endpoint with no OGC API - Styles surface. " +
4
+ "Build a MapLibre style client-side, or point honua-mcp at a Honua deployment for managed styles.";
2
5
  /** URI for the styles catalog resource (`honua://styles`). */
3
6
  export const uri = "honua://styles";
4
7
  /** URI template for a single style resource (`honua://styles/{styleId}`). */
@@ -8,7 +11,16 @@ export const uriTemplate = "honua://styles/{styleId}";
8
11
  * with its stable styleId, title, and canonical URI (`honua://styles/{id}`).
9
12
  */
10
13
  export async function readCatalog(client) {
11
- const list = await listStyles(client);
14
+ let list;
15
+ try {
16
+ list = await listStyles(client);
17
+ }
18
+ catch (err) {
19
+ if (err instanceof CapabilityUnavailableError) {
20
+ return unavailableContents(uri, err.reason);
21
+ }
22
+ throw err;
23
+ }
12
24
  const styles = list.styles.map((entry) => ({
13
25
  styleId: entry.id,
14
26
  title: entry.title ?? entry.id,
@@ -24,13 +36,34 @@ export async function readCatalog(client) {
24
36
  ],
25
37
  };
26
38
  }
39
+ /** Structured "not available on this target" resource body (platform-free mode). */
40
+ function unavailableContents(resourceUri, reason) {
41
+ return {
42
+ contents: [
43
+ {
44
+ uri: resourceUri,
45
+ mimeType: "application/json",
46
+ text: JSON.stringify(capabilityUnavailablePayload(STYLE_SURFACE, reason, STYLE_GUIDANCE), null, 2),
47
+ },
48
+ ],
49
+ };
50
+ }
27
51
  /**
28
52
  * Read a single style as a `StyleRef` projection (style_id / title /
29
53
  * description / style_version / encodings / legend_url), resolved against the
30
54
  * server's `/ogc/styles/{styleId}` surface.
31
55
  */
32
56
  export async function read(client, styleId) {
33
- const styleRef = await resolveStyleRef(client, styleId);
57
+ let styleRef;
58
+ try {
59
+ styleRef = await resolveStyleRef(client, styleId);
60
+ }
61
+ catch (err) {
62
+ if (err instanceof CapabilityUnavailableError) {
63
+ return unavailableContents(`honua://styles/${encodeURIComponent(styleId)}`, err.reason);
64
+ }
65
+ throw err;
66
+ }
34
67
  return {
35
68
  contents: [
36
69
  {
@@ -0,0 +1,14 @@
1
+ import type { JsonValue } from "@honua/sdk-js/query-planner";
2
+ /** Stable boundary error whose message never includes a reflected value or trap error. */
3
+ export declare class McpJsonSnapshotError extends Error {
4
+ constructor(message: string);
5
+ }
6
+ /**
7
+ * Detach untrusted JSON without invoking property getters.
8
+ *
9
+ * The clone is deeply frozen and bounded before any asynchronous authorization
10
+ * callback can observe it. Reflection failures (including Proxy traps) collapse
11
+ * to one stable error instead of reflecting attacker-controlled messages.
12
+ */
13
+ export declare function snapshotMcpJson(input: unknown, label: string): JsonValue;
14
+ //# sourceMappingURL=stable-json.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stable-json.d.ts","sourceRoot":"","sources":["../../src/stable-json.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,6BAA6B,CAAC;AAe7D,0FAA0F;AAC1F,qBAAa,oBAAqB,SAAQ,KAAK;gBAC1B,OAAO,EAAE,MAAM;CAKnC;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,GAAG,SAAS,CAOxE"}