@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.
Files changed (92) hide show
  1. package/README.md +92 -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/capability.d.ts +48 -0
  19. package/dist/src/capability.d.ts.map +1 -0
  20. package/dist/src/capability.js +44 -0
  21. package/dist/src/certification/census-data.d.ts +33 -0
  22. package/dist/src/certification/census-data.d.ts.map +1 -0
  23. package/dist/src/certification/census-data.js +506 -0
  24. package/dist/src/certification/census-fixture-client.d.ts +7 -0
  25. package/dist/src/certification/census-fixture-client.d.ts.map +1 -0
  26. package/dist/src/certification/census-fixture-client.js +226 -0
  27. package/dist/src/certification/certifier.d.ts +7 -1
  28. package/dist/src/certification/certifier.d.ts.map +1 -1
  29. package/dist/src/certification/certifier.js +52 -7
  30. package/dist/src/certification/cli.js +19 -4
  31. package/dist/src/certification/contracts.d.ts +41 -0
  32. package/dist/src/certification/contracts.d.ts.map +1 -1
  33. package/dist/src/certification/contracts.js +71 -0
  34. package/dist/src/certification/fixtures.d.ts +13 -0
  35. package/dist/src/certification/fixtures.d.ts.map +1 -1
  36. package/dist/src/certification/fixtures.js +28 -0
  37. package/dist/src/certification/json-schema.d.ts +23 -1
  38. package/dist/src/certification/json-schema.d.ts.map +1 -1
  39. package/dist/src/certification/json-schema.js +10 -2
  40. package/dist/src/certification/lifecycle.d.ts +14 -0
  41. package/dist/src/certification/lifecycle.d.ts.map +1 -0
  42. package/dist/src/certification/lifecycle.js +326 -0
  43. package/dist/src/certification/operator-catalog.d.ts +8 -0
  44. package/dist/src/certification/operator-catalog.d.ts.map +1 -1
  45. package/dist/src/certification/operator-catalog.js +189 -5
  46. package/dist/src/certification/report.d.ts.map +1 -1
  47. package/dist/src/certification/report.js +13 -0
  48. package/dist/src/certification/run.d.ts.map +1 -1
  49. package/dist/src/certification/run.js +1 -0
  50. package/dist/src/certification/schema-index.d.ts +25 -0
  51. package/dist/src/certification/schema-index.d.ts.map +1 -1
  52. package/dist/src/certification/schema-index.js +27 -0
  53. package/dist/src/certification/target.d.ts +9 -1
  54. package/dist/src/certification/target.d.ts.map +1 -1
  55. package/dist/src/certification/target.js +54 -2
  56. package/dist/src/eval/cli.js +4 -2
  57. package/dist/src/eval/corpus.d.ts +7 -0
  58. package/dist/src/eval/corpus.d.ts.map +1 -1
  59. package/dist/src/eval/corpus.js +16 -0
  60. package/dist/src/eval/drivers/deterministic.d.ts.map +1 -1
  61. package/dist/src/eval/drivers/deterministic.js +13 -0
  62. package/dist/src/eval/grade.d.ts.map +1 -1
  63. package/dist/src/eval/grade.js +20 -1
  64. package/dist/src/eval/report.d.ts +5 -0
  65. package/dist/src/eval/report.d.ts.map +1 -1
  66. package/dist/src/eval/report.js +3 -0
  67. package/dist/src/eval/runner.d.ts +6 -0
  68. package/dist/src/eval/runner.d.ts.map +1 -1
  69. package/dist/src/eval/runner.js +21 -7
  70. package/dist/src/eval/standalone-corpus.d.ts +3 -0
  71. package/dist/src/eval/standalone-corpus.d.ts.map +1 -0
  72. package/dist/src/eval/standalone-corpus.js +699 -0
  73. package/dist/src/eval/types.d.ts +25 -0
  74. package/dist/src/eval/types.d.ts.map +1 -1
  75. package/dist/src/index.d.ts +29 -5
  76. package/dist/src/index.d.ts.map +1 -1
  77. package/dist/src/index.js +41 -21
  78. package/dist/src/provenance.d.ts +44 -0
  79. package/dist/src/provenance.d.ts.map +1 -0
  80. package/dist/src/provenance.js +37 -0
  81. package/dist/src/resources/styles.d.ts.map +1 -1
  82. package/dist/src/resources/styles.js +36 -3
  83. package/dist/src/styles.d.ts +2 -0
  84. package/dist/src/styles.d.ts.map +1 -1
  85. package/dist/src/styles.js +29 -3
  86. package/dist/src/tools/apply-style-preset.d.ts.map +1 -1
  87. package/dist/src/tools/apply-style-preset.js +14 -2
  88. package/dist/src/tools/get-style.d.ts.map +1 -1
  89. package/dist/src/tools/get-style.js +24 -13
  90. package/dist/src/tools/list-services.d.ts.map +1 -1
  91. package/dist/src/tools/list-services.js +16 -1
  92. package/package.json +11 -2
@@ -0,0 +1,226 @@
1
+ import { CENSUS_EXTENT, CENSUS_FIELDS, CENSUS_LAYER_NAME, CENSUS_ROWS, CENSUS_SERVICE_ID, } from "./census-data.js";
2
+ /**
3
+ * Offline PLATFORM-FREE fixture `HonuaClient` (issue #369).
4
+ *
5
+ * Where {@link ./fixture-client.ts:createFixtureClient} models a Honua-enhanced
6
+ * deployment (it serves the OGC API – Styles surface), this client models a PLAIN
7
+ * public Esri FeatureServer — exactly what the standalone `honua-mcp` front door
8
+ * points at. It replays the recorded US Census apportionment layer
9
+ * (services.arcgis.com; see `census-data.ts`) through a tiny in-memory GeoServices
10
+ * evaluator so `count / query / extent / statistics` return REAL, deterministic
11
+ * answers with no network. Crucially it exposes NO Honua-only surface: every
12
+ * `pipelineFetch` (the OGC API – Styles path the style tools probe) 404s, so the
13
+ * capability-degradation path is exercised for real.
14
+ *
15
+ * The evaluator is verified against the live recordings in
16
+ * `test/certification/census-fixture-client.test.ts` (count=52, sum(pop)=335085841,
17
+ * sum(seats)=435, max=California, min=Wyoming), so the semantic eval assertions
18
+ * that ride on it are grounded, not hand-waved.
19
+ */
20
+ const SPATIAL_REFERENCE = { wkid: 102100, latestWkid: 3857 };
21
+ function fieldValue(row, field) {
22
+ return row[field];
23
+ }
24
+ function compare(actual, op, expected) {
25
+ if (actual === undefined || actual === null) {
26
+ // NULL never satisfies a comparison (matches GeoServices/SQL semantics).
27
+ return false;
28
+ }
29
+ if (typeof expected === "string" || typeof actual === "string") {
30
+ const a = String(actual);
31
+ const e = String(expected);
32
+ switch (op) {
33
+ case "=":
34
+ return a === e;
35
+ case "<>":
36
+ case "!=":
37
+ return a !== e;
38
+ default:
39
+ throw new Error(`census fixture: operator ${op} is not supported for string values`);
40
+ }
41
+ }
42
+ switch (op) {
43
+ case "=":
44
+ return actual === expected;
45
+ case "<>":
46
+ case "!=":
47
+ return actual !== expected;
48
+ case ">":
49
+ return actual > expected;
50
+ case ">=":
51
+ return actual >= expected;
52
+ case "<":
53
+ return actual < expected;
54
+ case "<=":
55
+ return actual <= expected;
56
+ default:
57
+ throw new Error(`census fixture: unsupported operator "${op}"`);
58
+ }
59
+ }
60
+ function matchClause(row, clause) {
61
+ const match = /^\s*([A-Za-z_][A-Za-z0-9_]*)\s*(>=|<=|<>|!=|=|>|<)\s*(.+?)\s*$/.exec(clause);
62
+ if (!match) {
63
+ throw new Error(`census fixture: unsupported WHERE clause "${clause}"`);
64
+ }
65
+ const [, field, op, rawValue] = match;
66
+ const actual = fieldValue(row, field);
67
+ if (actual === undefined) {
68
+ throw new Error(`census fixture: unknown field "${field}" in WHERE`);
69
+ }
70
+ const expected = /^'.*'$/.test(rawValue) ? rawValue.slice(1, -1) : Number(rawValue);
71
+ if (typeof expected === "number" && Number.isNaN(expected)) {
72
+ throw new Error(`census fixture: unparseable value in WHERE clause "${clause}"`);
73
+ }
74
+ return compare(actual, op, expected);
75
+ }
76
+ /** Filter the recorded rows by a (minimal) SQL WHERE clause. */
77
+ export function filterRows(where) {
78
+ const clause = (where ?? "1=1").trim();
79
+ if (clause === "" || clause === "1=1") {
80
+ return [...CENSUS_ROWS];
81
+ }
82
+ const conditions = clause.split(/\s+AND\s+/i);
83
+ return CENSUS_ROWS.filter((row) => conditions.every((condition) => matchClause(row, condition)));
84
+ }
85
+ function orderRows(rows, orderByFields) {
86
+ const specs = orderByFields.split(",").map((part) => {
87
+ const [field, dir] = part.trim().split(/\s+/);
88
+ return { field, descending: (dir ?? "ASC").toUpperCase() === "DESC" };
89
+ });
90
+ return [...rows].sort((a, b) => {
91
+ for (const { field, descending } of specs) {
92
+ const av = fieldValue(a, field);
93
+ const bv = fieldValue(b, field);
94
+ if (av === bv) {
95
+ continue;
96
+ }
97
+ const less = (av ?? 0) < (bv ?? 0);
98
+ return (less ? -1 : 1) * (descending ? -1 : 1);
99
+ }
100
+ return 0;
101
+ });
102
+ }
103
+ function aggregate(rows, type, field) {
104
+ // GeoServices aggregate statistics ignore NULL values (e.g. DC/PR seats).
105
+ const values = rows.map((row) => fieldValue(row, field)).filter((v) => typeof v === "number");
106
+ switch (type) {
107
+ case "count":
108
+ return values.length;
109
+ case "sum":
110
+ return values.reduce((acc, v) => acc + v, 0);
111
+ case "min":
112
+ return values.length > 0 ? Math.min(...values) : 0;
113
+ case "max":
114
+ return values.length > 0 ? Math.max(...values) : 0;
115
+ case "avg":
116
+ return values.reduce((acc, v) => acc + v, 0) / (values.length || 1);
117
+ case "stddev": {
118
+ const mean = values.reduce((acc, v) => acc + v, 0) / (values.length || 1);
119
+ const variance = values.reduce((acc, v) => acc + (v - mean) ** 2, 0) / (values.length || 1);
120
+ return Math.sqrt(variance);
121
+ }
122
+ default:
123
+ throw new Error(`census fixture: unsupported statisticType "${type}"`);
124
+ }
125
+ }
126
+ function computeStatistics(rows, request) {
127
+ const stats = request.outStatistics ?? [];
128
+ const groupField = request.groupByFieldsForStatistics?.split(",")[0]?.trim();
129
+ const build = (subset, groupValue) => {
130
+ const attributes = {};
131
+ if (groupField && groupValue !== undefined) {
132
+ attributes[groupField] = groupValue;
133
+ }
134
+ for (const stat of stats) {
135
+ attributes[stat.outStatisticFieldName] = aggregate(subset, stat.statisticType, stat.onStatisticField);
136
+ }
137
+ return { attributes };
138
+ };
139
+ if (!groupField) {
140
+ return { features: [build(rows)] };
141
+ }
142
+ const groups = new Map();
143
+ for (const row of rows) {
144
+ const key = fieldValue(row, groupField) ?? "";
145
+ const bucket = groups.get(key) ?? [];
146
+ bucket.push(row);
147
+ groups.set(key, bucket);
148
+ }
149
+ const features = [...groups.entries()].map(([value, subset]) => build(subset, value));
150
+ return { features };
151
+ }
152
+ function projectAttributes(row, outFields) {
153
+ if (outFields === undefined || outFields === "*" || (Array.isArray(outFields) && outFields.length === 0)) {
154
+ return { ...row };
155
+ }
156
+ const requested = Array.isArray(outFields) ? outFields : outFields.split(",").map((f) => f.trim());
157
+ const attributes = {};
158
+ for (const field of requested) {
159
+ const value = fieldValue(row, field);
160
+ if (value !== undefined) {
161
+ attributes[field] = value;
162
+ }
163
+ }
164
+ return attributes;
165
+ }
166
+ /** Build the offline platform-free fixture client (a plain public FeatureServer). */
167
+ export function createStandaloneFixtureClient() {
168
+ const client = {
169
+ serverBaseUrl: "https://services.arcgis.com",
170
+ async listServices() {
171
+ return { services: [{ name: CENSUS_SERVICE_ID, type: "FeatureServer" }] };
172
+ },
173
+ async getFeatureServiceMetadata(_serviceId) {
174
+ return {
175
+ serviceDescription: "US Census 2020 apportionment (public Esri Living Atlas FeatureServer)",
176
+ layers: [{ id: 0, name: CENSUS_LAYER_NAME }],
177
+ spatialReference: SPATIAL_REFERENCE,
178
+ };
179
+ },
180
+ async getLayerMetadata(_serviceId, _layerId) {
181
+ return {
182
+ id: 0,
183
+ name: CENSUS_LAYER_NAME,
184
+ description: "One row per state (plus DC and Puerto Rico) with 2020 population and apportioned House seats.",
185
+ geometryType: "esriGeometryPolygon",
186
+ fields: CENSUS_FIELDS.map((f) => ({ ...f })),
187
+ extent: CENSUS_EXTENT,
188
+ spatialReference: SPATIAL_REFERENCE,
189
+ relationships: [],
190
+ };
191
+ },
192
+ async queryFeatures(request = {}) {
193
+ const rows = filterRows(request.where);
194
+ const extra = request.extraParams ?? {};
195
+ if (extra.returnCountOnly === true) {
196
+ return { count: rows.length };
197
+ }
198
+ if (extra.returnExtentOnly === true) {
199
+ return { count: rows.length, extent: CENSUS_EXTENT };
200
+ }
201
+ if (request.outStatistics && request.outStatistics.length > 0) {
202
+ return computeStatistics(rows, request);
203
+ }
204
+ const ordered = request.orderByFields ? orderRows(rows, request.orderByFields) : rows;
205
+ const offset = request.resultOffset ?? 0;
206
+ const limit = request.resultRecordCount ?? ordered.length;
207
+ const page = ordered.slice(offset, offset + limit);
208
+ return {
209
+ objectIdFieldName: "OBJECTID",
210
+ geometryType: "esriGeometryPolygon",
211
+ spatialReference: SPATIAL_REFERENCE,
212
+ features: page.map((row) => ({ attributes: projectAttributes(row, request.outFields) })),
213
+ exceededTransferLimit: false,
214
+ };
215
+ },
216
+ // No Honua-only surface: the OGC API - Styles probe always 404s so the
217
+ // capability-degradation path is exercised against a real plain endpoint.
218
+ async pipelineFetch(_method, _path) {
219
+ return new Response(JSON.stringify({ error: { code: 404, message: "not found" } }), {
220
+ status: 404,
221
+ headers: { "Content-Type": "application/json" },
222
+ });
223
+ },
224
+ };
225
+ return client;
226
+ }
@@ -1,4 +1,5 @@
1
1
  import type { Client } from "@modelcontextprotocol/sdk/client/index.js";
2
+ import { type AuthMode, type SuiteProvenance } from "../provenance.js";
2
3
  import { AUTH_ERROR_CODES } from "./contracts.js";
3
4
  import type { CertTargetMode } from "./target.js";
4
5
  /** Per-tool certification record (machine-readable). */
@@ -37,7 +38,7 @@ export interface KnownGap {
37
38
  }
38
39
  /** A cross-cutting contract check against the certified surface. */
39
40
  export interface ContractCheck {
40
- contract: "output-schema" | "error-shape" | "auth-unauthenticated" | "list-pagination";
41
+ contract: "output-schema" | "error-shape" | "auth-unauthenticated" | "list-pagination" | "query-pagination" | "mutating-round-trip" | "mutating-permission-denied" | "async-job-lifecycle";
41
42
  target: string;
42
43
  status: "passed" | "failed" | "skipped";
43
44
  detail: string;
@@ -61,6 +62,8 @@ export interface CertificationReport {
61
62
  indexDate: string;
62
63
  dialect: string;
63
64
  };
65
+ /** Self-proving provenance: what was certified, with which suite, how authed. */
66
+ provenance: SuiteProvenance;
64
67
  summary: {
65
68
  pass: boolean;
66
69
  toolsDiscovered: number;
@@ -74,6 +77,7 @@ export interface CertificationReport {
74
77
  contractsChecked: number;
75
78
  contractsPassed: number;
76
79
  contractsFailed: number;
80
+ contractsSkipped: number;
77
81
  knownGaps: number;
78
82
  failures: number;
79
83
  };
@@ -99,6 +103,8 @@ export interface CertifyOptions {
99
103
  honuaTransport?: string | null;
100
104
  /** Open a fresh, unauthenticated client for the auth contract (optional). */
101
105
  connectUnauthenticated?: (() => Promise<Client>) | undefined;
106
+ /** How the authenticated client reached the surface (recorded in provenance). */
107
+ authMode?: AuthMode;
102
108
  /** Environment used to resolve round-trip identifiers. */
103
109
  env?: NodeJS.ProcessEnv;
104
110
  }
@@ -1 +1 @@
1
- {"version":3,"file":"certifier.d.ts","sourceRoot":"","sources":["../../../src/certification/certifier.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"certifier.d.ts","sourceRoot":"","sources":["../../../src/certification/certifier.ts"],"names":[],"mappings":"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"}
@@ -1,7 +1,9 @@
1
+ import { readNegotiatedProtocolVersion, resolveSuiteGitSha, } from "../provenance.js";
1
2
  import { AUTH_ERROR_CODES, checkAuthContract, checkInvalidArgsContract, checkPaginationContract, } from "./contracts.js";
2
3
  import { buildInvalidArgsInputs, buildRoundTripInputs, resolveRoundTripEnv } from "./fixtures.js";
3
4
  import { checkConformance, checkWellFormed, validateAgainstSchema } from "./json-schema.js";
4
- import { buildReferenceToolLookup, loadSchemaFile, loadSchemaIndex, } from "./schema-index.js";
5
+ import { runDeepContracts } from "./lifecycle.js";
6
+ import { PLATFORM_FREE_DEGRADATIONS, buildReferenceToolLookup, loadSchemaFile, loadSchemaIndex, } from "./schema-index.js";
5
7
  const STANDARD_SOURCE = "geospatial-mcp@54cbd49 (spec/schemas)";
6
8
  /** Determine whether a discovered tool is safe to round-trip (read-only). */
7
9
  export function isReadOnlyTool(tool) {
@@ -26,11 +28,15 @@ export async function certify(options) {
26
28
  const { tools: advertisedTools, pages: toolPages } = await listAllTools(client);
27
29
  const { resources: advertisedResources, pages: resourcePages } = await listAllResources(client);
28
30
  const prompts = await listPromptsSafely(client);
31
+ // The platform-free standalone target has no Honua publishing surface, so
32
+ // reference-shape tools (e.g. apply_style_preset) intentionally degrade their
33
+ // platform-bound required properties. Conformance is evaluated accordingly.
34
+ const platformFree = options.targetMode === "standalone";
29
35
  const tools = [];
30
36
  const advertisedNames = new Set();
31
37
  for (const tool of advertisedTools) {
32
38
  advertisedNames.add(tool.name);
33
- tools.push(await certifyTool(client, tool, referenceLookup, roundTripInputs));
39
+ tools.push(await certifyTool(client, tool, referenceLookup, roundTripInputs, platformFree));
34
40
  }
35
41
  const resources = advertisedResources.map((r) => ({
36
42
  name: r.name,
@@ -41,9 +47,29 @@ export async function certify(options) {
41
47
  contracts.push(paginationCheck("tools", toolPages));
42
48
  contracts.push(paginationCheck("resources", resourcePages));
43
49
  collectOutputSchemaContracts(tools, contracts);
44
- contracts.push(await runErrorShapeContract(client, advertisedTools));
50
+ contracts.push(await runErrorShapeContract(client, advertisedTools, options.targetMode));
45
51
  await runAuthContracts(options, advertisedTools, advertisedResources, contracts);
52
+ // Deep contracts: mutating round-trip, unauthenticated-write refusal, async job
53
+ // lifecycle, and query pagination. Safe by construction — mutations only run
54
+ // against the disposable offline mock or an explicit scratch target.
55
+ const deep = await runDeepContracts({
56
+ client,
57
+ advertisedToolNames: advertisedNames,
58
+ isDisposableBackend: options.backend === "mock-upstream",
59
+ connectUnauthenticated: options.connectUnauthenticated,
60
+ env: options.env,
61
+ });
62
+ contracts.push(...deep);
46
63
  const knownGaps = collectKnownGaps(index, advertisedNames, referenceLookup);
64
+ const gitSha = resolveSuiteGitSha(options.env);
65
+ const provenance = {
66
+ suiteGitSha: gitSha.sha,
67
+ suiteGitShaSource: gitSha.source,
68
+ targetUrl: options.surface,
69
+ protocolVersion: readNegotiatedProtocolVersion(client),
70
+ toolCount: advertisedTools.length,
71
+ authMode: options.authMode ?? (options.backend === "mock-upstream" ? "bearer" : "unknown"),
72
+ };
47
73
  return assembleReport({
48
74
  serverInfo: serverInfo,
49
75
  index,
@@ -53,6 +79,7 @@ export async function certify(options) {
53
79
  prompts,
54
80
  contracts,
55
81
  knownGaps,
82
+ provenance,
56
83
  });
57
84
  }
58
85
  async function listAllTools(client) {
@@ -88,7 +115,7 @@ async function listPromptsSafely(client) {
88
115
  return [];
89
116
  }
90
117
  }
91
- async function certifyTool(client, tool, referenceLookup, roundTripInputs) {
118
+ async function certifyTool(client, tool, referenceLookup, roundTripInputs, platformFree) {
92
119
  const errors = [];
93
120
  const notes = [];
94
121
  const wellFormed = checkWellFormed(tool.inputSchema);
@@ -110,7 +137,8 @@ async function certifyTool(client, tool, referenceLookup, roundTripInputs) {
110
137
  if (standardEntry) {
111
138
  const standardSchema = loadSchemaFile(standardEntry.schema);
112
139
  if (wellFormed.wellFormed) {
113
- const result = checkConformance(tool.inputSchema, standardSchema);
140
+ const shedRequired = platformFree ? PLATFORM_FREE_DEGRADATIONS[tool.name] : undefined;
141
+ const result = checkConformance(tool.inputSchema, standardSchema, { shedRequired });
114
142
  conformant = result.conformant;
115
143
  if (!result.conformant) {
116
144
  errors.push(...result.violations.map((v) => `conformance: ${v}`));
@@ -212,7 +240,21 @@ function collectOutputSchemaContracts(tools, contracts) {
212
240
  });
213
241
  }
214
242
  }
215
- async function runErrorShapeContract(client, advertised) {
243
+ async function runErrorShapeContract(client, advertised, targetMode) {
244
+ // The platform-free standalone surface is built on the high-level MCP server,
245
+ // which rejects invalid arguments with a JSON-RPC -32602 (InvalidParams) error
246
+ // BEFORE the tool body runs — a valid MCP-native structured error, but not the
247
+ // Honua GeoprocessingError envelope this contract asserts. That envelope is a
248
+ // Honua-operator richness a plain FeatureServer does not (and need not) provide,
249
+ // so we skip-with-reason rather than fail. Other targets are unaffected.
250
+ if (targetMode === "standalone") {
251
+ return {
252
+ contract: "error-shape",
253
+ target: "(standalone)",
254
+ status: "skipped",
255
+ detail: "standalone surface uses MCP-native input validation (JSON-RPC -32602 InvalidParams); the GeoprocessingError envelope is a Honua-operator surface feature",
256
+ };
257
+ }
216
258
  const invalidInputs = buildInvalidArgsInputs();
217
259
  const advertisedNames = new Set(advertised.map((t) => t.name));
218
260
  const target = Object.keys(invalidInputs).find((name) => advertisedNames.has(name));
@@ -359,7 +401,7 @@ function collectKnownGaps(index, advertisedNames, referenceLookup) {
359
401
  return gaps;
360
402
  }
361
403
  function assembleReport(args) {
362
- const { serverInfo, index, options, tools, resources, prompts, contracts, knownGaps } = args;
404
+ const { serverInfo, index, options, tools, resources, prompts, contracts, knownGaps, provenance } = args;
363
405
  const toolsSchemaValid = tools.filter((t) => t.schemaValid).length;
364
406
  const conformanceChecked = tools.filter((t) => t.conformant !== null);
365
407
  const toolsConformant = conformanceChecked.filter((t) => t.conformant === true).length;
@@ -368,6 +410,7 @@ function assembleReport(args) {
368
410
  const contractsChecked = contracts.filter((c) => c.status !== "skipped").length;
369
411
  const contractsPassed = contracts.filter((c) => c.status === "passed").length;
370
412
  const contractsFailed = contracts.filter((c) => c.status === "failed").length;
413
+ const contractsSkipped = contracts.filter((c) => c.status === "skipped").length;
371
414
  const toolFailures = tools.reduce((acc, t) => acc + t.errors.length, 0);
372
415
  const failures = toolFailures + contractsFailed;
373
416
  const pass = failures === 0;
@@ -383,6 +426,7 @@ function assembleReport(args) {
383
426
  surface: options.surface,
384
427
  },
385
428
  standard: { source: STANDARD_SOURCE, indexDate: index.date, dialect: index.dialect },
429
+ provenance,
386
430
  summary: {
387
431
  pass,
388
432
  toolsDiscovered: tools.length,
@@ -396,6 +440,7 @@ function assembleReport(args) {
396
440
  contractsChecked,
397
441
  contractsPassed,
398
442
  contractsFailed,
443
+ contractsSkipped,
399
444
  knownGaps: knownGaps.length,
400
445
  failures,
401
446
  },
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ import { mkdirSync } from "node:fs";
2
3
  import { resolve } from "node:path";
3
4
  import { fileURLToPath } from "node:url";
4
5
  import { runCertification, writeArtifacts } from "./run.js";
@@ -6,6 +7,7 @@ function parseArgs(argv) {
6
7
  let artifactOnly = false;
7
8
  // Default to the package root (two levels up from dist/src/certification/).
8
9
  let outDir = fileURLToPath(new URL("../../../", import.meta.url));
10
+ let target;
9
11
  for (let i = 0; i < argv.length; i++) {
10
12
  const arg = argv[i];
11
13
  if (arg === "--artifact-only") {
@@ -18,8 +20,15 @@ function parseArgs(argv) {
18
20
  }
19
21
  outDir = resolve(value);
20
22
  }
23
+ else if (arg === "--target") {
24
+ const value = argv[++i];
25
+ if (!value) {
26
+ throw new Error("--target requires a target mode (offline | remote | stdio-proxy | standalone)");
27
+ }
28
+ target = value;
29
+ }
21
30
  }
22
- return { artifactOnly, outDir };
31
+ return { artifactOnly, outDir, ...(target ? { target } : {}) };
23
32
  }
24
33
  function artifactPaths(outDir) {
25
34
  return {
@@ -28,15 +37,21 @@ function artifactPaths(outDir) {
28
37
  };
29
38
  }
30
39
  async function main() {
31
- const { artifactOnly, outDir } = parseArgs(process.argv.slice(2));
32
- const report = await runCertification();
40
+ const { artifactOnly, outDir, target } = parseArgs(process.argv.slice(2));
41
+ const env = target ? { ...process.env, HONUA_MCP_CERT_TARGET: target } : process.env;
42
+ const report = await runCertification({ env });
43
+ mkdirSync(outDir, { recursive: true });
33
44
  const paths = artifactPaths(outDir);
34
45
  writeArtifacts(report, paths);
35
46
  const s = report.summary;
36
47
  const status = s.pass ? "PASS" : "FAIL";
37
48
  process.stdout.write(`MCP certification ${status} [${report.protocol.targetMode}]: ${s.toolsConformant}/${s.toolsConformanceChecked} conformant, ` +
38
49
  `${s.toolsOutputValidated} output-validated, ${s.contractsPassed}/${s.contractsChecked} contracts, ` +
39
- `${s.knownGaps} known gaps, ${s.failures} failures.\n`);
50
+ `${s.contractsSkipped} skipped, ${s.knownGaps} known gaps, ${s.failures} failures.\n`);
51
+ const skipped = report.contracts.filter((c) => c.status === "skipped");
52
+ for (const c of skipped) {
53
+ process.stdout.write(` SKIP [${c.contract} / ${c.target}]: ${c.detail}\n`);
54
+ }
40
55
  process.stdout.write(`Artifacts: ${paths.json}\n ${paths.markdown}\n`);
41
56
  if (!artifactOnly && !report.summary.pass) {
42
57
  process.exitCode = 1;
@@ -49,6 +49,47 @@ export declare function checkInvalidArgsContract(result: {
49
49
  * envelope or as an AuthorizationDenied geoprocessing-error kind.
50
50
  */
51
51
  export declare function checkAuthContract(source: unknown): ContractOutcome;
52
+ /** Documented job states, ordered by lifecycle rank (terminal states are highest). */
53
+ export declare const JOB_STATE_RANK: Record<string, number>;
54
+ /** Observations gathered from a mutating insert→verify→update→verify→delete→verify round-trip. */
55
+ export interface FeatureRoundTripObservations {
56
+ /** Object id assigned by the insert (undefined ⇒ insert did not return one). */
57
+ insertedObjectId: number | undefined;
58
+ /** The inserted feature was found by a follow-up query. */
59
+ presentAfterInsert: boolean;
60
+ /** Attribute value read back after the update. */
61
+ attributeAfterUpdate: unknown;
62
+ /** Attribute value the update wrote (what `attributeAfterUpdate` must equal). */
63
+ expectedAttribute: unknown;
64
+ /** The feature was absent from a follow-up query after the delete. */
65
+ absentAfterDelete: boolean;
66
+ }
67
+ /**
68
+ * mutating-round-trip contract: an insert must be observable, an update must be
69
+ * reflected, and a delete must remove the feature — the full edit lifecycle
70
+ * verified against the surface's own read path.
71
+ */
72
+ export declare function checkFeatureRoundTrip(obs: FeatureRoundTripObservations): ContractOutcome;
73
+ /** Observations gathered from an async job execute→poll→result lifecycle. */
74
+ export interface JobLifecycleObservations {
75
+ /** Status returned synchronously by execute (e.g. "Submitted"). */
76
+ initialStatus: string;
77
+ /** Statuses observed by polling the job resource, in poll order. */
78
+ polledStatuses: string[];
79
+ /** Progress fractions observed alongside the polled statuses (0..1). */
80
+ progressFractions: number[];
81
+ /** The lifecycle reached a documented terminal state. */
82
+ finalStatus: string;
83
+ /** A result package was fetchable and reported ready once the job succeeded. */
84
+ resultReady: boolean;
85
+ }
86
+ /**
87
+ * async-job-lifecycle contract: an execute → poll(honua://jobs/{id}) → fetch-result
88
+ * flow must follow the documented state machine — statuses are all recognized,
89
+ * rank never regresses, progress is monotonic within [0,1], the job terminates
90
+ * (Succeeded here), and the result package is retrievable.
91
+ */
92
+ export declare function checkJobLifecycle(obs: JobLifecycleObservations): ContractOutcome;
52
93
  export interface ListPage {
53
94
  /** Distinct item identity keys observed on this page (e.g. tool names). */
54
95
  keys: string[];
@@ -1 +1 @@
1
- {"version":3,"file":"contracts.d.ts","sourceRoot":"","sources":["../../../src/certification/contracts.ts"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;GAeG;AAEH,mFAAmF;AACnF,eAAO,MAAM,gBAAgB,mDAAoD,CAAC;AAElF,8EAA8E;AAC9E,eAAO,MAAM,gBAAgB,yEAA0E,CAAC;AAUxG,MAAM,WAAW,uBAAuB;IACtC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAMD;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,OAAO,GAAG,uBAAuB,GAAG,SAAS,CAgC3F;AAED,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,OAAO,CAAC;IACZ,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAED,wEAAwE;AACxE,wBAAgB,0BAA0B,CAAC,KAAK,EAAE,OAAO,GAAG,eAAe,CAG1E;AAED;;;;GAIG;AACH,wBAAgB,wBAAwB,CAAC,MAAM,EAAE;IAC/C,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,iBAAiB,CAAC,EAAE,OAAO,CAAC;CAC7B,GAAG,eAAe,CA8BlB;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,OAAO,GAAG,eAAe,CAoBlE;AAED,MAAM,WAAW,QAAQ;IACvB,2EAA2E;IAC3E,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,UAAU,EAAE,MAAM,GAAG,SAAS,CAAC;CAChC;AAED;;;GAGG;AACH,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,QAAQ,EAAE,GAAG,eAAe,CA6B1E"}
1
+ {"version":3,"file":"contracts.d.ts","sourceRoot":"","sources":["../../../src/certification/contracts.ts"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;GAeG;AAEH,mFAAmF;AACnF,eAAO,MAAM,gBAAgB,mDAAoD,CAAC;AAElF,8EAA8E;AAC9E,eAAO,MAAM,gBAAgB,yEAA0E,CAAC;AAUxG,MAAM,WAAW,uBAAuB;IACtC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAMD;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,OAAO,GAAG,uBAAuB,GAAG,SAAS,CAgC3F;AAED,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,OAAO,CAAC;IACZ,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAED,wEAAwE;AACxE,wBAAgB,0BAA0B,CAAC,KAAK,EAAE,OAAO,GAAG,eAAe,CAG1E;AAED;;;;GAIG;AACH,wBAAgB,wBAAwB,CAAC,MAAM,EAAE;IAC/C,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,iBAAiB,CAAC,EAAE,OAAO,CAAC;CAC7B,GAAG,eAAe,CA8BlB;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,OAAO,GAAG,eAAe,CAoBlE;AAED,sFAAsF;AACtF,eAAO,MAAM,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAOjD,CAAC;AAGF,kGAAkG;AAClG,MAAM,WAAW,4BAA4B;IAC3C,gFAAgF;IAChF,gBAAgB,EAAE,MAAM,GAAG,SAAS,CAAC;IACrC,2DAA2D;IAC3D,kBAAkB,EAAE,OAAO,CAAC;IAC5B,kDAAkD;IAClD,oBAAoB,EAAE,OAAO,CAAC;IAC9B,iFAAiF;IACjF,iBAAiB,EAAE,OAAO,CAAC;IAC3B,sEAAsE;IACtE,iBAAiB,EAAE,OAAO,CAAC;CAC5B;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,4BAA4B,GAAG,eAAe,CAmBxF;AAED,6EAA6E;AAC7E,MAAM,WAAW,wBAAwB;IACvC,mEAAmE;IACnE,aAAa,EAAE,MAAM,CAAC;IACtB,oEAAoE;IACpE,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,wEAAwE;IACxE,iBAAiB,EAAE,MAAM,EAAE,CAAC;IAC5B,yDAAyD;IACzD,WAAW,EAAE,MAAM,CAAC;IACpB,gFAAgF;IAChF,WAAW,EAAE,OAAO,CAAC;CACtB;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,wBAAwB,GAAG,eAAe,CAiChF;AAED,MAAM,WAAW,QAAQ;IACvB,2EAA2E;IAC3E,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,UAAU,EAAE,MAAM,GAAG,SAAS,CAAC;CAChC;AAED;;;GAGG;AACH,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,QAAQ,EAAE,GAAG,eAAe,CA6B1E"}
@@ -135,6 +135,77 @@ export function checkAuthContract(source) {
135
135
  ],
136
136
  };
137
137
  }
138
+ /** Documented job states, ordered by lifecycle rank (terminal states are highest). */
139
+ export const JOB_STATE_RANK = {
140
+ Submitted: 0,
141
+ Queued: 0,
142
+ Running: 1,
143
+ Succeeded: 2,
144
+ Failed: 2,
145
+ Cancelled: 2,
146
+ };
147
+ const TERMINAL_JOB_STATES = new Set(["Succeeded", "Failed", "Cancelled"]);
148
+ /**
149
+ * mutating-round-trip contract: an insert must be observable, an update must be
150
+ * reflected, and a delete must remove the feature — the full edit lifecycle
151
+ * verified against the surface's own read path.
152
+ */
153
+ export function checkFeatureRoundTrip(obs) {
154
+ const errors = [];
155
+ if (obs.insertedObjectId === undefined) {
156
+ errors.push("insert did not return a new objectId");
157
+ }
158
+ if (!obs.presentAfterInsert) {
159
+ errors.push("inserted feature was not observable via a follow-up query");
160
+ }
161
+ if (obs.attributeAfterUpdate !== obs.expectedAttribute) {
162
+ errors.push(`updated attribute did not round-trip (expected ${JSON.stringify(obs.expectedAttribute)}, read ${JSON.stringify(obs.attributeAfterUpdate)})`);
163
+ }
164
+ if (!obs.absentAfterDelete) {
165
+ errors.push("feature was still observable after delete");
166
+ }
167
+ return { ok: errors.length === 0, errors };
168
+ }
169
+ /**
170
+ * async-job-lifecycle contract: an execute → poll(honua://jobs/{id}) → fetch-result
171
+ * flow must follow the documented state machine — statuses are all recognized,
172
+ * rank never regresses, progress is monotonic within [0,1], the job terminates
173
+ * (Succeeded here), and the result package is retrievable.
174
+ */
175
+ export function checkJobLifecycle(obs) {
176
+ const errors = [];
177
+ const allStatuses = [obs.initialStatus, ...obs.polledStatuses, obs.finalStatus];
178
+ let lastRank = -1;
179
+ for (const status of allStatuses) {
180
+ const rank = JOB_STATE_RANK[status];
181
+ if (rank === undefined) {
182
+ errors.push(`unrecognized job state "${status}" (not in the documented state machine)`);
183
+ continue;
184
+ }
185
+ if (rank < lastRank) {
186
+ errors.push(`job state regressed from rank ${lastRank} to "${status}" (rank ${rank})`);
187
+ }
188
+ lastRank = rank;
189
+ }
190
+ let lastFraction = -1;
191
+ for (const fraction of obs.progressFractions) {
192
+ if (typeof fraction !== "number" || Number.isNaN(fraction) || fraction < 0 || fraction > 1) {
193
+ errors.push(`progress fraction ${String(fraction)} is out of the documented [0,1] range`);
194
+ continue;
195
+ }
196
+ if (fraction < lastFraction) {
197
+ errors.push(`progress fraction regressed from ${lastFraction} to ${fraction}`);
198
+ }
199
+ lastFraction = fraction;
200
+ }
201
+ if (!TERMINAL_JOB_STATES.has(obs.finalStatus)) {
202
+ errors.push(`job did not reach a terminal state (ended "${obs.finalStatus}")`);
203
+ }
204
+ if (obs.finalStatus === "Succeeded" && !obs.resultReady) {
205
+ errors.push("job succeeded but no ready result package could be fetched");
206
+ }
207
+ return { ok: errors.length === 0, errors };
208
+ }
138
209
  /**
139
210
  * pagination contract: following `nextCursor` yields additional distinct items,
140
211
  * never repeats a cursor (terminates), and the final page has no cursor.
@@ -22,6 +22,19 @@ export declare function resolveRoundTripEnv(env?: NodeJS.ProcessEnv): RoundTripE
22
22
  * listed here.
23
23
  */
24
24
  export declare function buildRoundTripInputs(env: RoundTripEnv): Record<string, Record<string, unknown>>;
25
+ /** Target layer for the mutating round-trip + pagination contracts. */
26
+ export interface EditTarget {
27
+ serviceId: string;
28
+ layerId: number;
29
+ }
30
+ /**
31
+ * Resolve the editable/pageable target for the deep contracts. Defaults to the
32
+ * offline mock's seeded `svc-parks` layer 0; override with
33
+ * `HONUA_MCP_EDIT_SERVICE_ID` / `HONUA_MCP_EDIT_LAYER_ID` (or the shared
34
+ * `HONUA_MCP_SERVICE_ID` / `HONUA_MCP_LAYER_ID`) to point a live mutating cert at
35
+ * a dedicated scratch service.
36
+ */
37
+ export declare function resolveEditTarget(env?: NodeJS.ProcessEnv): EditTarget;
25
38
  /**
26
39
  * A deliberately invalid argument set for the error-shape contract. Certification
27
40
  * picks the first advertised tool that has an entry here and calls it with these
@@ -1 +1 @@
1
- {"version":3,"file":"fixtures.d.ts","sourceRoot":"","sources":["../../../src/certification/fixtures.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,MAAM,WAAW,YAAY;IAC3B,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,GAAG,SAAS,CAAC;CAC7B;AAED,gFAAgF;AAChF,wBAAgB,mBAAmB,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,YAAY,CAQtF;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,YAAY,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAyB/F;AAED;;;;;GAKG;AACH,wBAAgB,sBAAsB,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAMhF"}
1
+ {"version":3,"file":"fixtures.d.ts","sourceRoot":"","sources":["../../../src/certification/fixtures.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,MAAM,WAAW,YAAY;IAC3B,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,GAAG,SAAS,CAAC;CAC7B;AAED,gFAAgF;AAChF,wBAAgB,mBAAmB,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,YAAY,CAQtF;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,YAAY,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAsC/F;AAED,uEAAuE;AACvE,MAAM,WAAW,UAAU;IACzB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,UAAU,CAOlF;AAED;;;;;GAKG;AACH,wBAAgB,sBAAsB,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAMhF"}