@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
@@ -26,6 +26,19 @@ export function resolveRoundTripEnv(env = process.env) {
26
26
  export function buildRoundTripInputs(env) {
27
27
  const { serviceId, layerId, address } = env;
28
28
  return {
29
+ // ── Platform-free standalone surface tools (issue #369). Round-tripped by the
30
+ // `standalone` cert target against a plain public FeatureServer fixture. The
31
+ // Honua-only style tools resolve to a structured "not available on this
32
+ // target" result (non-error) — certifying graceful degradation, not a crash.
33
+ honua_list_services: {},
34
+ honua_describe_layer: { serviceId, layerId },
35
+ honua_count_features: { serviceId, layerId },
36
+ honua_get_extent: { serviceId, layerId },
37
+ honua_statistics: { serviceId, layerId, statisticType: "count", onField: "OBJECTID" },
38
+ honua_explain_capability_gap: { capability: "query", protocol: "wmts" },
39
+ honua_get_style: {},
40
+ honua_apply_style_preset: { styleId: "topographic" },
41
+ // ── Honua-enhanced operator catalog tools (offline/remote/stdio-proxy targets).
29
42
  honua_list_layers: {},
30
43
  honua_list_capabilities: {},
31
44
  honua_query_features: { serviceId, layerId, limit: 1 },
@@ -49,6 +62,21 @@ export function buildRoundTripInputs(env) {
49
62
  honua_resolve_entity: { text: "parks" },
50
63
  };
51
64
  }
65
+ /**
66
+ * Resolve the editable/pageable target for the deep contracts. Defaults to the
67
+ * offline mock's seeded `svc-parks` layer 0; override with
68
+ * `HONUA_MCP_EDIT_SERVICE_ID` / `HONUA_MCP_EDIT_LAYER_ID` (or the shared
69
+ * `HONUA_MCP_SERVICE_ID` / `HONUA_MCP_LAYER_ID`) to point a live mutating cert at
70
+ * a dedicated scratch service.
71
+ */
72
+ export function resolveEditTarget(env = process.env) {
73
+ const base = resolveRoundTripEnv(env);
74
+ const serviceId = env.HONUA_MCP_EDIT_SERVICE_ID ?? base.serviceId;
75
+ const layerRaw = env.HONUA_MCP_EDIT_LAYER_ID;
76
+ const parsedLayer = layerRaw !== undefined ? Number.parseInt(layerRaw, 10) : Number.NaN;
77
+ const layerId = Number.isFinite(parsedLayer) && parsedLayer >= 0 ? parsedLayer : base.layerId;
78
+ return { serviceId, layerId };
79
+ }
52
80
  /**
53
81
  * A deliberately invalid argument set for the error-shape contract. Certification
54
82
  * picks the first advertised tool that has an entry here and calls it with these
@@ -27,6 +27,28 @@ export interface ConformanceResult {
27
27
  /** Non-fatal observations (e.g. advertised extra properties, type widenings). */
28
28
  notes: string[];
29
29
  }
30
+ export interface ConformanceOptions {
31
+ /**
32
+ * Required standard properties this surface is permitted to omit because it is
33
+ * a documented, platform-gated degradation of the tool (see the SDK-local
34
+ * `PLATFORM_FREE_DEGRADATIONS` registry in `schema-index.ts`).
35
+ *
36
+ * A property listed here is a platform *binding* — an identifier that only has
37
+ * meaning against a Honua publishing/authoring surface (e.g. `apply_style_preset`
38
+ * requires `serviceId`+`layerId` to bind a preset onto a *published* layer via
39
+ * the server OGC API – Styles authoring surface, ADR-0048). A platform-free
40
+ * target has no such surface, so it degrades the tool to a non-error, client-side
41
+ * projection keyed on the taxonomy essence alone (e.g. `styleId`) and legitimately
42
+ * omits the binding identifiers.
43
+ *
44
+ * When a required standard property is absent AND listed here, it is recorded as
45
+ * a degradation note rather than a conformance violation. Everything else stays
46
+ * strict: unlisted required properties still fail, and type incompatibilities on
47
+ * properties the surface DOES advertise remain violations — degradation relaxes
48
+ * platform binding, never correctness on what is accepted.
49
+ */
50
+ shedRequired?: readonly string[];
51
+ }
30
52
  /**
31
53
  * Deterministically compare an advertised tool inputSchema against a standard
32
54
  * geospatial-mcp tool schema.
@@ -45,5 +67,5 @@ export interface ConformanceResult {
45
67
  * notes, not violations: a richer reference shape is allowed to extend the
46
68
  * bare standard.
47
69
  */
48
- export declare function checkConformance(advertised: JsonSchema, standard: JsonSchema): ConformanceResult;
70
+ export declare function checkConformance(advertised: JsonSchema, standard: JsonSchema, options?: ConformanceOptions): ConformanceResult;
49
71
  //# sourceMappingURL=json-schema.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"json-schema.d.ts","sourceRoot":"","sources":["../../../src/certification/json-schema.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAA2C,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAElG,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAwBpD,MAAM,WAAW,gBAAgB;IAC/B,UAAU,EAAE,OAAO,CAAC;IACpB,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,OAAO,GAAG,gBAAgB,CAcjE;AAED,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,OAAO,CAAC;IACf,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AASD,gFAAgF;AAChF,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,UAAU,GAAG,gBAAgB,CAGrE;AAED,gFAAgF;AAChF,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,UAAU,EAAE,KAAK,EAAE,OAAO,GAAG,gBAAgB,CAQ1F;AAmCD,MAAM,WAAW,iBAAiB;IAChC,UAAU,EAAE,OAAO,CAAC;IACpB,iEAAiE;IACjE,eAAe,EAAE,MAAM,EAAE,CAAC;IAC1B,iFAAiF;IACjF,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,iFAAiF;IACjF,KAAK,EAAE,MAAM,EAAE,CAAC;CACjB;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,UAAU,EAAE,QAAQ,EAAE,UAAU,GAAG,iBAAiB,CAwDhG"}
1
+ {"version":3,"file":"json-schema.d.ts","sourceRoot":"","sources":["../../../src/certification/json-schema.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAA2C,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAElG,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAwBpD,MAAM,WAAW,gBAAgB;IAC/B,UAAU,EAAE,OAAO,CAAC;IACpB,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,OAAO,GAAG,gBAAgB,CAcjE;AAED,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,OAAO,CAAC;IACf,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AASD,gFAAgF;AAChF,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,UAAU,GAAG,gBAAgB,CAGrE;AAED,gFAAgF;AAChF,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,UAAU,EAAE,KAAK,EAAE,OAAO,GAAG,gBAAgB,CAQ1F;AAmCD,MAAM,WAAW,iBAAiB;IAChC,UAAU,EAAE,OAAO,CAAC;IACpB,iEAAiE;IACjE,eAAe,EAAE,MAAM,EAAE,CAAC;IAC1B,iFAAiF;IACjF,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,iFAAiF;IACjF,KAAK,EAAE,MAAM,EAAE,CAAC;CACjB;AAED,MAAM,WAAW,kBAAkB;IACjC;;;;;;;;;;;;;;;;;;OAkBG;IACH,YAAY,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAClC;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,gBAAgB,CAC9B,UAAU,EAAE,UAAU,EACtB,QAAQ,EAAE,UAAU,EACpB,OAAO,GAAE,kBAAuB,GAC/B,iBAAiB,CAkEnB"}
@@ -112,10 +112,13 @@ function schemaTypes(schema) {
112
112
  * notes, not violations: a richer reference shape is allowed to extend the
113
113
  * bare standard.
114
114
  */
115
- export function checkConformance(advertised, standard) {
115
+ export function checkConformance(advertised, standard, options = {}) {
116
116
  const violations = [];
117
117
  const notes = [];
118
118
  const matchedRequired = [];
119
+ // Required standard properties this surface may legitimately shed as a
120
+ // documented platform-gated degradation. See {@link ConformanceOptions.shedRequired}.
121
+ const shedRequired = new Set(options.shedRequired ?? []);
119
122
  const standardRequired = requiredPropertyNames(standard);
120
123
  const standardProps = (standard.properties ?? {});
121
124
  const advertisedProps = (advertised.properties ?? {});
@@ -123,7 +126,12 @@ export function checkConformance(advertised, standard) {
123
126
  for (const name of standardRequired) {
124
127
  const present = advertisedNames ? advertisedNames.has(name) : name in advertisedProps;
125
128
  if (!present) {
126
- violations.push(`missing required standard property "${name}"`);
129
+ if (shedRequired.has(name)) {
130
+ notes.push(`degrades platform binding: omits required "${name}" (platform-free target has no publishing surface)`);
131
+ }
132
+ else {
133
+ violations.push(`missing required standard property "${name}"`);
134
+ }
127
135
  continue;
128
136
  }
129
137
  const standardTypes = schemaTypes(standardProps[name]);
@@ -0,0 +1,14 @@
1
+ import type { Client } from "@modelcontextprotocol/sdk/client/index.js";
2
+ import type { ContractCheck } from "./certifier.js";
3
+ export interface DeepContractOptions {
4
+ client: Client;
5
+ advertisedToolNames: Set<string>;
6
+ /** Whether the backend is the disposable offline mock (auto-allows mutation). */
7
+ isDisposableBackend: boolean;
8
+ /** Open a fresh unauthenticated client (for the permission-denied case). */
9
+ connectUnauthenticated?: (() => Promise<Client>) | undefined;
10
+ env?: NodeJS.ProcessEnv;
11
+ }
12
+ /** Run all deep contracts, returning one ContractCheck per contract (never throws). */
13
+ export declare function runDeepContracts(options: DeepContractOptions): Promise<ContractCheck[]>;
14
+ //# sourceMappingURL=lifecycle.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lifecycle.d.ts","sourceRoot":"","sources":["../../../src/certification/lifecycle.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAC;AACxE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAoCpD,MAAM,WAAW,mBAAmB;IAClC,MAAM,EAAE,MAAM,CAAC;IACf,mBAAmB,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IACjC,iFAAiF;IACjF,mBAAmB,EAAE,OAAO,CAAC;IAC7B,4EAA4E;IAC5E,sBAAsB,CAAC,EAAE,CAAC,MAAM,OAAO,CAAC,MAAM,CAAC,CAAC,GAAG,SAAS,CAAC;IAC7D,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;CACzB;AAcD,uFAAuF;AACvF,wBAAsB,gBAAgB,CAAC,OAAO,EAAE,mBAAmB,GAAG,OAAO,CAAC,aAAa,EAAE,CAAC,CAW7F"}
@@ -0,0 +1,326 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { checkAuthContract, checkFeatureRoundTrip, checkJobLifecycle, checkPaginationContract, } from "./contracts.js";
3
+ import { resolveEditTarget } from "./fixtures.js";
4
+ /**
5
+ * Deep, stateful certification contracts (A5 provability): the mutating-tool
6
+ * round-trip, the async job lifecycle, and query pagination. These extend the
7
+ * read-side contracts in {@link certifier} into the parts of a real deployment
8
+ * that were "never certified" — writes and asynchronous execution.
9
+ *
10
+ * Safety: mutating contracts only auto-run against the offline mock (a disposable
11
+ * fixture). Against a live/remote target they are gated behind
12
+ * `HONUA_MCP_CERT_ALLOW_MUTATION=1` + a caller-supplied scratch service, so a
13
+ * scheduled cert against a shared demo never mutates real data. When a target
14
+ * lacks the tool, the contract SKIPS with a loud reason rather than failing — so
15
+ * the suite stays runnable against any revision. Note: the reference honua-server
16
+ * `/mcp` surface does NOT advertise `EDIT_TOOL` — Honua does not support AI
17
+ * operational data editing (honua-server ADR-0028) — so against it these mutating
18
+ * contracts skip. `EDIT_TOOL` here represents the standard's OPTIONAL `mutation`
19
+ * profile, exercised via a mutation-profile-adopter fixture.
20
+ */
21
+ const MUTATION_ALLOW_ENV = "HONUA_MCP_CERT_ALLOW_MUTATION";
22
+ const EDIT_TOOL = "honua_edit_features";
23
+ const QUERY_TOOL = "honua_query_features";
24
+ const EXECUTE_TOOL = "honua_execute_plan";
25
+ const MAX_JOB_POLLS = 12;
26
+ const PAGE_LIMIT = 2;
27
+ const MAX_FEATURE_PAGES = 20;
28
+ function isTruthy(value) {
29
+ if (typeof value !== "string") {
30
+ return false;
31
+ }
32
+ const v = value.trim().toLowerCase();
33
+ return v.length > 0 && v !== "0" && v !== "false" && v !== "no" && v !== "off";
34
+ }
35
+ function skip(contract, target, detail) {
36
+ return { contract, target, status: "skipped", detail };
37
+ }
38
+ /** Run all deep contracts, returning one ContractCheck per contract (never throws). */
39
+ export async function runDeepContracts(options) {
40
+ const env = options.env ?? process.env;
41
+ const target = resolveEditTarget(env);
42
+ const mutationAllowed = options.isDisposableBackend || isTruthy(env[MUTATION_ALLOW_ENV]);
43
+ return [
44
+ await runMutatingRoundTrip(options, target, mutationAllowed),
45
+ await runMutatingPermissionDenied(options),
46
+ await runJobLifecycle(options, mutationAllowed),
47
+ await runQueryPagination(options, target),
48
+ ];
49
+ }
50
+ async function callTool(client, name, args) {
51
+ const result = (await client.callTool({ name, arguments: args }));
52
+ return result;
53
+ }
54
+ async function readJsonResource(client, uri) {
55
+ const res = (await client.readResource({ uri }));
56
+ const text = res.contents?.[0]?.text;
57
+ return typeof text === "string" ? JSON.parse(text) : {};
58
+ }
59
+ function featureAttributes(result) {
60
+ const features = (result.structuredContent?.features ?? []);
61
+ return features.map((f) => f.attributes ?? {});
62
+ }
63
+ // ── Contract 1: mutating-tool insert→verify→update→verify→delete→verify ──────
64
+ async function runMutatingRoundTrip(options, target, mutationAllowed) {
65
+ const contract = "mutating-round-trip";
66
+ if (!options.advertisedToolNames.has(EDIT_TOOL) || !options.advertisedToolNames.has(QUERY_TOOL)) {
67
+ return skip(contract, EDIT_TOOL, `${EDIT_TOOL}/${QUERY_TOOL} not advertised by this surface (pre-P1 or read-only)`);
68
+ }
69
+ if (!mutationAllowed) {
70
+ return skip(contract, EDIT_TOOL, `mutating contract disabled for this target — set ${MUTATION_ALLOW_ENV}=1 with a scratch service to certify writes`);
71
+ }
72
+ const { client } = options;
73
+ const marker = `cert-${randomUUID().slice(0, 8)}`;
74
+ const updatedMarker = `${marker}-updated`;
75
+ let objectId;
76
+ try {
77
+ const inserted = await callTool(client, EDIT_TOOL, {
78
+ serviceId: target.serviceId,
79
+ layerId: target.layerId,
80
+ adds: [{ attributes: { NAME: marker } }],
81
+ });
82
+ const addResults = (inserted.structuredContent?.addResults ?? []);
83
+ objectId = typeof addResults[0]?.objectId === "number" ? addResults[0].objectId : undefined;
84
+ const presentAfterInsert = objectId !== undefined &&
85
+ featureAttributes(await callTool(client, QUERY_TOOL, {
86
+ serviceId: target.serviceId,
87
+ layerId: target.layerId,
88
+ objectIds: [objectId],
89
+ })).some((a) => Number(a.OBJECTID) === objectId);
90
+ if (objectId !== undefined) {
91
+ await callTool(client, EDIT_TOOL, {
92
+ serviceId: target.serviceId,
93
+ layerId: target.layerId,
94
+ updates: [{ objectId, attributes: { NAME: updatedMarker } }],
95
+ });
96
+ }
97
+ const attributeAfterUpdate = objectId === undefined
98
+ ? undefined
99
+ : featureAttributes(await callTool(client, QUERY_TOOL, {
100
+ serviceId: target.serviceId,
101
+ layerId: target.layerId,
102
+ objectIds: [objectId],
103
+ }))[0]?.NAME;
104
+ if (objectId !== undefined) {
105
+ await callTool(client, EDIT_TOOL, { serviceId: target.serviceId, layerId: target.layerId, deletes: [objectId] });
106
+ }
107
+ const absentAfterDelete = objectId !== undefined &&
108
+ featureAttributes(await callTool(client, QUERY_TOOL, {
109
+ serviceId: target.serviceId,
110
+ layerId: target.layerId,
111
+ objectIds: [objectId],
112
+ })).every((a) => Number(a.OBJECTID) !== objectId);
113
+ const outcome = checkFeatureRoundTrip({
114
+ insertedObjectId: objectId,
115
+ presentAfterInsert,
116
+ attributeAfterUpdate,
117
+ expectedAttribute: updatedMarker,
118
+ absentAfterDelete,
119
+ });
120
+ return {
121
+ contract,
122
+ target: EDIT_TOOL,
123
+ status: outcome.ok ? "passed" : "failed",
124
+ detail: outcome.ok
125
+ ? `insert→verify→update→verify→delete→verify round-trip succeeded (objectId ${objectId})`
126
+ : outcome.errors.join("; "),
127
+ };
128
+ }
129
+ catch (err) {
130
+ return {
131
+ contract,
132
+ target: EDIT_TOOL,
133
+ status: "failed",
134
+ detail: `mutating round-trip raised: ${err instanceof Error ? err.message : String(err)}`,
135
+ };
136
+ }
137
+ finally {
138
+ // Guaranteed cleanup: remove the scratch feature even if an assertion threw.
139
+ if (objectId !== undefined) {
140
+ await callTool(client, EDIT_TOOL, {
141
+ serviceId: target.serviceId,
142
+ layerId: target.layerId,
143
+ deletes: [objectId],
144
+ }).catch(() => { });
145
+ }
146
+ }
147
+ }
148
+ // ── Contract 1b: mutating call without credentials → structured auth error ───
149
+ async function runMutatingPermissionDenied(options) {
150
+ const contract = "mutating-permission-denied";
151
+ if (!options.advertisedToolNames.has(EDIT_TOOL)) {
152
+ return skip(contract, EDIT_TOOL, `${EDIT_TOOL} not advertised by this surface (pre-P1)`);
153
+ }
154
+ if (!options.connectUnauthenticated) {
155
+ return skip(contract, EDIT_TOOL, "target does not support an unauthenticated pass");
156
+ }
157
+ const target = resolveEditTarget(options.env ?? process.env);
158
+ let unauth;
159
+ try {
160
+ unauth = await options.connectUnauthenticated();
161
+ // An unauthenticated write MUST be refused with a structured auth signal. If
162
+ // the boundary holds, nothing is mutated.
163
+ let result;
164
+ try {
165
+ result = await callTool(unauth, EDIT_TOOL, {
166
+ serviceId: target.serviceId,
167
+ layerId: target.layerId,
168
+ adds: [{ attributes: { NAME: "cert-should-be-denied" } }],
169
+ });
170
+ }
171
+ catch (err) {
172
+ const outcome = checkAuthContract(err);
173
+ return {
174
+ contract,
175
+ target: EDIT_TOOL,
176
+ status: outcome.ok ? "passed" : "failed",
177
+ detail: outcome.ok ? "unauthenticated write refused with a structured auth error" : outcome.errors.join("; "),
178
+ };
179
+ }
180
+ if (result.isError !== true) {
181
+ return {
182
+ contract,
183
+ target: EDIT_TOOL,
184
+ status: "failed",
185
+ detail: "unauthenticated write returned a non-error result — the write auth boundary is not enforced",
186
+ };
187
+ }
188
+ const outcome = checkAuthContract(result);
189
+ return {
190
+ contract,
191
+ target: EDIT_TOOL,
192
+ status: outcome.ok ? "passed" : "failed",
193
+ detail: outcome.ok ? "unauthenticated write refused with a structured auth error" : outcome.errors.join("; "),
194
+ };
195
+ }
196
+ catch (err) {
197
+ return {
198
+ contract,
199
+ target: EDIT_TOOL,
200
+ status: "failed",
201
+ detail: `permission-denied pass could not be established: ${err instanceof Error ? err.message : String(err)}`,
202
+ };
203
+ }
204
+ finally {
205
+ if (unauth) {
206
+ await unauth.close().catch(() => { });
207
+ }
208
+ }
209
+ }
210
+ // ── Contract 2: async job lifecycle execute → poll → result ──────────────────
211
+ async function runJobLifecycle(options, mutationAllowed) {
212
+ const contract = "async-job-lifecycle";
213
+ if (!options.advertisedToolNames.has(EXECUTE_TOOL)) {
214
+ return skip(contract, EXECUTE_TOOL, `${EXECUTE_TOOL} not advertised by this surface (pre-P1)`);
215
+ }
216
+ if (!mutationAllowed) {
217
+ return skip(contract, EXECUTE_TOOL, `job lifecycle disabled for this target — set ${MUTATION_ALLOW_ENV}=1 to certify execution against a scratch target`);
218
+ }
219
+ const { client } = options;
220
+ try {
221
+ const submitted = await callTool(client, EXECUTE_TOOL, {
222
+ plan: { planId: "cert-plan", steps: [{ stepId: "s1", kind: "QueryFeatures" }] },
223
+ });
224
+ const jobId = submitted.structuredContent?.jobId;
225
+ const initialStatus = String(submitted.structuredContent?.status ?? "Unknown");
226
+ if (typeof jobId !== "string") {
227
+ return { contract, target: EXECUTE_TOOL, status: "failed", detail: "execute did not return a jobId" };
228
+ }
229
+ const jobUri = `honua://jobs/${jobId}`;
230
+ const polledStatuses = [];
231
+ const progressFractions = [];
232
+ let finalStatus = initialStatus;
233
+ let resultUri;
234
+ for (let i = 0; i < MAX_JOB_POLLS; i++) {
235
+ const state = await readJsonResource(client, jobUri);
236
+ const status = String(state.status ?? "Unknown");
237
+ polledStatuses.push(status);
238
+ const fraction = state.progress?.fraction;
239
+ if (typeof fraction === "number") {
240
+ progressFractions.push(fraction);
241
+ }
242
+ finalStatus = status;
243
+ if (typeof state.resultUri === "string") {
244
+ resultUri = state.resultUri;
245
+ }
246
+ if (["Succeeded", "Failed", "Cancelled"].includes(status)) {
247
+ break;
248
+ }
249
+ }
250
+ let resultReady = false;
251
+ if (resultUri) {
252
+ const pkg = await readJsonResource(client, resultUri);
253
+ resultReady = String(pkg.status ?? "") === "Ready";
254
+ }
255
+ const outcome = checkJobLifecycle({
256
+ initialStatus,
257
+ polledStatuses,
258
+ progressFractions,
259
+ finalStatus,
260
+ resultReady,
261
+ });
262
+ return {
263
+ contract,
264
+ target: EXECUTE_TOOL,
265
+ status: outcome.ok ? "passed" : "failed",
266
+ detail: outcome.ok
267
+ ? `execute→poll→result honored the state machine (${[initialStatus, ...polledStatuses].join("→")}; result ready)`
268
+ : outcome.errors.join("; "),
269
+ };
270
+ }
271
+ catch (err) {
272
+ return {
273
+ contract,
274
+ target: EXECUTE_TOOL,
275
+ status: "failed",
276
+ detail: `job lifecycle raised: ${err instanceof Error ? err.message : String(err)}`,
277
+ };
278
+ }
279
+ }
280
+ // ── Contract 3: query_features pagination (read-only, safe against any target) ─
281
+ async function runQueryPagination(options, target) {
282
+ const contract = "query-pagination";
283
+ if (!options.advertisedToolNames.has(QUERY_TOOL)) {
284
+ return skip(contract, QUERY_TOOL, `${QUERY_TOOL} not advertised by this surface`);
285
+ }
286
+ const { client } = options;
287
+ try {
288
+ const pages = [];
289
+ let cursor;
290
+ for (let i = 0; i < MAX_FEATURE_PAGES; i++) {
291
+ const args = { serviceId: target.serviceId, layerId: target.layerId, limit: PAGE_LIMIT };
292
+ if (cursor) {
293
+ args.cursor = cursor;
294
+ }
295
+ const result = await callTool(client, QUERY_TOOL, args);
296
+ const keys = featureAttributes(result).map((a) => String(a.OBJECTID));
297
+ const nextCursor = result.structuredContent?.nextCursor;
298
+ pages.push({ keys, nextCursor: typeof nextCursor === "string" ? nextCursor : undefined });
299
+ if (typeof nextCursor !== "string") {
300
+ break;
301
+ }
302
+ cursor = nextCursor;
303
+ }
304
+ const paginated = pages.length > 1 || pages[0]?.nextCursor !== undefined;
305
+ if (!paginated) {
306
+ return skip(contract, QUERY_TOOL, `surface returned a single page with no nextCursor (pagination not exercised — pre-P1 surface or fewer than ${PAGE_LIMIT} features)`);
307
+ }
308
+ const outcome = checkPaginationContract(pages);
309
+ return {
310
+ contract,
311
+ target: QUERY_TOOL,
312
+ status: outcome.ok ? "passed" : "failed",
313
+ detail: outcome.ok
314
+ ? `query_features paginated across ${pages.length} pages; nextCursor honored and terminated`
315
+ : outcome.errors.join("; "),
316
+ };
317
+ }
318
+ catch (err) {
319
+ return {
320
+ contract,
321
+ target: QUERY_TOOL,
322
+ status: "failed",
323
+ detail: `query pagination raised: ${err instanceof Error ? err.message : String(err)}`,
324
+ };
325
+ }
326
+ }
@@ -15,6 +15,14 @@ import { type JsonSchema } from "./schema-index.js";
15
15
  * conformance check exercises the real standard shape rather than a hand-rolled
16
16
  * approximation.
17
17
  *
18
+ * NOTE ON `honua_edit_features`: the real honua-server `/mcp` surface exposes NO
19
+ * feature-mutation tool — Honua does not support AI operational data editing
20
+ * (honua-server ADR-0028). This fixture nonetheless advertises `honua_edit_features`
21
+ * so the certifier keeps exercising the standard's OPTIONAL `mutation`-profile
22
+ * certification path (insert→update→delete round-trip, auth-boundary contract)
23
+ * for *other* adopters that choose to offer governed editing. It is a
24
+ * mutation-profile-adopter fixture, NOT a mirror of honua-server's product surface.
25
+ *
18
26
  * Nothing here is a product server. It is a labeled test fixture whose only job
19
27
  * is to give the certifier a faithful `/mcp`-shaped surface to certify offline.
20
28
  */
@@ -1 +1 @@
1
- {"version":3,"file":"operator-catalog.d.ts","sourceRoot":"","sources":["../../../src/certification/operator-catalog.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAC;AAUnE,OAAO,EAAE,KAAK,UAAU,EAA6D,MAAM,mBAAmB,CAAC;AAE/G;;;;;;;;;;;;;;;;;GAiBG;AAEH,gFAAgF;AAChF,MAAM,WAAW,uBAAuB;IACtC;;;OAGG;IACH,IAAI,EAAE,iBAAiB,GAAG,mBAAmB,GAAG,kBAAkB,GAAG,WAAW,GAAG,kBAAkB,CAAC;IACtG,+FAA+F;IAC/F,KAAK,EAAE;QACL,IAAI,EACA,kBAAkB,GAClB,qBAAqB,GACrB,gBAAgB,GAChB,gBAAgB,GAChB,iBAAiB,GACjB,SAAS,GACT,WAAW,GACX,qBAAqB,CAAC;QAC1B,OAAO,EAAE,MAAM,CAAC;QAChB,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,UAAU,CAAC,EAAE;YAAE,IAAI,EAAE,MAAM,CAAC;YAAC,OAAO,EAAE,MAAM,CAAC;YAAC,SAAS,CAAC,EAAE,MAAM,CAAA;SAAE,EAAE,CAAC;KACtE,CAAC;CACH;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,UAAU,CAAC;IACxB,YAAY,EAAE,UAAU,CAAC;IACzB,QAAQ,EAAE,OAAO,CAAC;IAClB,6EAA6E;IAC7E,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACvC;AA0BD;;;;;;GAMG;AACH,wBAAgB,kBAAkB,IAAI,YAAY,EAAE,CAwSnD;AAED,MAAM,WAAW,gBAAgB;IAC/B,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,OAAO,CAAC;CACnB;AAED,wBAAgB,sBAAsB,IAAI,gBAAgB,EAAE,CA2B3D;AAED,2EAA2E;AAC3E,eAAO,MAAM,iBAAiB,KAAK,CAAC;AAoCpC,MAAM,WAAW,sBAAsB;IACrC;;;;OAIG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED;;;;;GAKG;AACH,wBAAgB,2BAA2B,CAAC,OAAO,GAAE,sBAA2B,GAAG,MAAM,CA+ExF"}
1
+ {"version":3,"file":"operator-catalog.d.ts","sourceRoot":"","sources":["../../../src/certification/operator-catalog.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAC;AAUnE,OAAO,EAAE,KAAK,UAAU,EAA6D,MAAM,mBAAmB,CAAC;AAE/G;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,gFAAgF;AAChF,MAAM,WAAW,uBAAuB;IACtC;;;OAGG;IACH,IAAI,EAAE,iBAAiB,GAAG,mBAAmB,GAAG,kBAAkB,GAAG,WAAW,GAAG,kBAAkB,CAAC;IACtG,+FAA+F;IAC/F,KAAK,EAAE;QACL,IAAI,EACA,kBAAkB,GAClB,qBAAqB,GACrB,gBAAgB,GAChB,gBAAgB,GAChB,iBAAiB,GACjB,SAAS,GACT,WAAW,GACX,qBAAqB,CAAC;QAC1B,OAAO,EAAE,MAAM,CAAC;QAChB,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,UAAU,CAAC,EAAE;YAAE,IAAI,EAAE,MAAM,CAAC;YAAC,OAAO,EAAE,MAAM,CAAC;YAAC,SAAS,CAAC,EAAE,MAAM,CAAA;SAAE,EAAE,CAAC;KACtE,CAAC;CACH;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,UAAU,CAAC;IACxB,YAAY,EAAE,UAAU,CAAC;IACzB,QAAQ,EAAE,OAAO,CAAC;IAClB,6EAA6E;IAC7E,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACvC;AA0BD;;;;;;GAMG;AACH,wBAAgB,kBAAkB,IAAI,YAAY,EAAE,CA6UnD;AAOD,MAAM,WAAW,gBAAgB;IAC/B,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,OAAO,CAAC;CACnB;AAED,wBAAgB,sBAAsB,IAAI,gBAAgB,EAAE,CA2B3D;AAED,2EAA2E;AAC3E,eAAO,MAAM,iBAAiB,KAAK,CAAC;AAoCpC,MAAM,WAAW,sBAAsB;IACrC;;;;OAIG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAuHD;;;;;GAKG;AACH,wBAAgB,2BAA2B,CAAC,OAAO,GAAE,sBAA2B,GAAG,MAAM,CAsGxF"}