@honua/mcp-server 0.0.20-alpha.0 → 0.1.0-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.
- package/README.md +92 -15
- package/certification/geospatial-mcp-schemas/PROVENANCE.md +15 -7
- package/certification/geospatial-mcp-schemas/conformance/manifest.schema.json +6 -0
- package/certification/geospatial-mcp-schemas/index.json +88 -9
- package/certification/geospatial-mcp-schemas/tools/apply_style_preset.schema.json +13 -9
- package/certification/geospatial-mcp-schemas/tools/buffer_features.schema.json +60 -0
- package/certification/geospatial-mcp-schemas/tools/edit_features.schema.json +74 -0
- package/certification/geospatial-mcp-schemas/tools/export_dataset.schema.json +60 -0
- package/certification/geospatial-mcp-schemas/tools/geocode_addresses.schema.json +34 -0
- package/certification/geospatial-mcp-schemas/tools/get_style.schema.json +13 -3
- package/certification/geospatial-mcp-schemas/tools/ingest_dataset.schema.json +45 -0
- package/certification/geospatial-mcp-schemas/tools/join_features.schema.json +73 -0
- package/certification/geospatial-mcp-schemas/tools/overlay_features.schema.json +53 -0
- package/certification/geospatial-mcp-schemas/tools/query_features.schema.json +20 -0
- package/certification/geospatial-mcp-schemas/tools/render_map.schema.json +5 -0
- package/certification/geospatial-mcp-schemas/tools/reproject_features.schema.json +49 -0
- package/certification/geospatial-mcp-schemas/tools/summarize_statistics.schema.json +71 -0
- package/dist/src/capability.d.ts +48 -0
- package/dist/src/capability.d.ts.map +1 -0
- package/dist/src/capability.js +44 -0
- package/dist/src/certification/census-data.d.ts +33 -0
- package/dist/src/certification/census-data.d.ts.map +1 -0
- package/dist/src/certification/census-data.js +506 -0
- package/dist/src/certification/census-fixture-client.d.ts +7 -0
- package/dist/src/certification/census-fixture-client.d.ts.map +1 -0
- package/dist/src/certification/census-fixture-client.js +226 -0
- package/dist/src/certification/certifier.d.ts +7 -1
- package/dist/src/certification/certifier.d.ts.map +1 -1
- package/dist/src/certification/certifier.js +52 -7
- package/dist/src/certification/cli.js +19 -4
- package/dist/src/certification/contracts.d.ts +41 -0
- package/dist/src/certification/contracts.d.ts.map +1 -1
- package/dist/src/certification/contracts.js +71 -0
- package/dist/src/certification/fixtures.d.ts +13 -0
- package/dist/src/certification/fixtures.d.ts.map +1 -1
- package/dist/src/certification/fixtures.js +28 -0
- package/dist/src/certification/json-schema.d.ts +23 -1
- package/dist/src/certification/json-schema.d.ts.map +1 -1
- package/dist/src/certification/json-schema.js +10 -2
- package/dist/src/certification/lifecycle.d.ts +14 -0
- package/dist/src/certification/lifecycle.d.ts.map +1 -0
- package/dist/src/certification/lifecycle.js +326 -0
- package/dist/src/certification/operator-catalog.d.ts +8 -0
- package/dist/src/certification/operator-catalog.d.ts.map +1 -1
- package/dist/src/certification/operator-catalog.js +189 -5
- package/dist/src/certification/report.d.ts.map +1 -1
- package/dist/src/certification/report.js +13 -0
- package/dist/src/certification/run.d.ts.map +1 -1
- package/dist/src/certification/run.js +1 -0
- package/dist/src/certification/schema-index.d.ts +25 -0
- package/dist/src/certification/schema-index.d.ts.map +1 -1
- package/dist/src/certification/schema-index.js +27 -0
- package/dist/src/certification/target.d.ts +9 -1
- package/dist/src/certification/target.d.ts.map +1 -1
- package/dist/src/certification/target.js +54 -2
- package/dist/src/eval/cli.js +4 -2
- package/dist/src/eval/corpus.d.ts +7 -0
- package/dist/src/eval/corpus.d.ts.map +1 -1
- package/dist/src/eval/corpus.js +16 -0
- package/dist/src/eval/drivers/deterministic.d.ts.map +1 -1
- package/dist/src/eval/drivers/deterministic.js +13 -0
- package/dist/src/eval/grade.d.ts.map +1 -1
- package/dist/src/eval/grade.js +20 -1
- package/dist/src/eval/report.d.ts +5 -0
- package/dist/src/eval/report.d.ts.map +1 -1
- package/dist/src/eval/report.js +3 -0
- package/dist/src/eval/runner.d.ts +6 -0
- package/dist/src/eval/runner.d.ts.map +1 -1
- package/dist/src/eval/runner.js +21 -7
- package/dist/src/eval/standalone-corpus.d.ts +3 -0
- package/dist/src/eval/standalone-corpus.d.ts.map +1 -0
- package/dist/src/eval/standalone-corpus.js +699 -0
- package/dist/src/eval/types.d.ts +25 -0
- package/dist/src/eval/types.d.ts.map +1 -1
- package/dist/src/index.d.ts +29 -5
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +41 -21
- package/dist/src/provenance.d.ts +44 -0
- package/dist/src/provenance.d.ts.map +1 -0
- package/dist/src/provenance.js +37 -0
- package/dist/src/resources/styles.d.ts.map +1 -1
- package/dist/src/resources/styles.js +36 -3
- package/dist/src/styles.d.ts +2 -0
- package/dist/src/styles.d.ts.map +1 -1
- package/dist/src/styles.js +29 -3
- package/dist/src/tools/apply-style-preset.d.ts.map +1 -1
- package/dist/src/tools/apply-style-preset.js +14 -2
- package/dist/src/tools/get-style.d.ts.map +1 -1
- package/dist/src/tools/get-style.js +24 -13
- package/dist/src/tools/list-services.d.ts.map +1 -1
- package/dist/src/tools/list-services.js +16 -1
- package/package.json +11 -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,
|
|
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
|
-
|
|
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
|
|
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"}
|