@honua/mcp-server 0.0.9-alpha.0 → 0.0.20-alpha.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 (147) hide show
  1. package/README.md +167 -0
  2. package/certification/geospatial-mcp-schemas/PROVENANCE.md +33 -0
  3. package/certification/geospatial-mcp-schemas/README.md +90 -0
  4. package/certification/geospatial-mcp-schemas/common/geoprocessing-error.schema.json +45 -0
  5. package/certification/geospatial-mcp-schemas/conformance/manifest.schema.json +73 -0
  6. package/certification/geospatial-mcp-schemas/index.json +311 -0
  7. package/certification/geospatial-mcp-schemas/resources/app-package.schema.json +20 -0
  8. package/certification/geospatial-mcp-schemas/resources/app-template.schema.json +15 -0
  9. package/certification/geospatial-mcp-schemas/resources/artifact.schema.json +69 -0
  10. package/certification/geospatial-mcp-schemas/resources/deployment.schema.json +27 -0
  11. package/certification/geospatial-mcp-schemas/resources/map-package.schema.json +21 -0
  12. package/certification/geospatial-mcp-schemas/resources/map-template.schema.json +15 -0
  13. package/certification/geospatial-mcp-schemas/resources/provenance.schema.json +50 -0
  14. package/certification/geospatial-mcp-schemas/resources/published-service.schema.json +21 -0
  15. package/certification/geospatial-mcp-schemas/resources/result-package.schema.json +64 -0
  16. package/certification/geospatial-mcp-schemas/resources/style.schema.json +12 -0
  17. package/certification/geospatial-mcp-schemas/resources/theme.schema.json +12 -0
  18. package/certification/geospatial-mcp-schemas/resources/workspace.schema.json +63 -0
  19. package/certification/geospatial-mcp-schemas/tools/apply_style_preset.schema.json +23 -0
  20. package/certification/geospatial-mcp-schemas/tools/cancel_job.schema.json +15 -0
  21. package/certification/geospatial-mcp-schemas/tools/clarify_intent.schema.json +69 -0
  22. package/certification/geospatial-mcp-schemas/tools/compose_mixed_protocol_map.schema.json +21 -0
  23. package/certification/geospatial-mcp-schemas/tools/create_app_package.schema.json +33 -0
  24. package/certification/geospatial-mcp-schemas/tools/create_map_package.schema.json +41 -0
  25. package/certification/geospatial-mcp-schemas/tools/execute_plan.schema.json +64 -0
  26. package/certification/geospatial-mcp-schemas/tools/geocode_address.schema.json +36 -0
  27. package/certification/geospatial-mcp-schemas/tools/get_style.schema.json +30 -0
  28. package/certification/geospatial-mcp-schemas/tools/ground_candidates.schema.json +66 -0
  29. package/certification/geospatial-mcp-schemas/tools/list_capabilities.schema.json +21 -0
  30. package/certification/geospatial-mcp-schemas/tools/list_layers.schema.json +14 -0
  31. package/certification/geospatial-mcp-schemas/tools/plan_analysis.schema.json +19 -0
  32. package/certification/geospatial-mcp-schemas/tools/preview_app_package.schema.json +15 -0
  33. package/certification/geospatial-mcp-schemas/tools/preview_map_package.schema.json +17 -0
  34. package/certification/geospatial-mcp-schemas/tools/preview_package.schema.json +40 -0
  35. package/certification/geospatial-mcp-schemas/tools/propose_operation.schema.json +28 -0
  36. package/certification/geospatial-mcp-schemas/tools/publish_result.schema.json +29 -0
  37. package/certification/geospatial-mcp-schemas/tools/publish_service.schema.json +59 -0
  38. package/certification/geospatial-mcp-schemas/tools/query_features.schema.json +54 -0
  39. package/certification/geospatial-mcp-schemas/tools/refine_map_package.schema.json +22 -0
  40. package/certification/geospatial-mcp-schemas/tools/render_map.schema.json +55 -0
  41. package/certification/geospatial-mcp-schemas/tools/resolve_entity.schema.json +30 -0
  42. package/certification/geospatial-mcp-schemas/tools/solve_route.schema.json +39 -0
  43. package/certification/geospatial-mcp-schemas/tools/validate_package.schema.json +35 -0
  44. package/certification/geospatial-mcp-schemas/tools/validate_plan.schema.json +61 -0
  45. package/dist/src/certification/certifier.d.ts +119 -0
  46. package/dist/src/certification/certifier.d.ts.map +1 -0
  47. package/dist/src/certification/certifier.js +410 -0
  48. package/dist/src/certification/cli.d.ts +3 -0
  49. package/dist/src/certification/cli.d.ts.map +1 -0
  50. package/dist/src/certification/cli.js +48 -0
  51. package/dist/src/certification/contracts.d.ts +62 -0
  52. package/dist/src/certification/contracts.d.ts.map +1 -0
  53. package/dist/src/certification/contracts.js +174 -0
  54. package/dist/src/certification/fixture-client.d.ts +4 -0
  55. package/dist/src/certification/fixture-client.d.ts.map +1 -0
  56. package/dist/src/certification/fixture-client.js +113 -0
  57. package/dist/src/certification/fixtures.d.ts +32 -0
  58. package/dist/src/certification/fixtures.d.ts.map +1 -0
  59. package/dist/src/certification/fixtures.js +64 -0
  60. package/dist/src/certification/json-schema.d.ts +49 -0
  61. package/dist/src/certification/json-schema.d.ts.map +1 -0
  62. package/dist/src/certification/json-schema.js +164 -0
  63. package/dist/src/certification/mock-upstream.d.ts +29 -0
  64. package/dist/src/certification/mock-upstream.d.ts.map +1 -0
  65. package/dist/src/certification/mock-upstream.js +96 -0
  66. package/dist/src/certification/operator-catalog.d.ts +82 -0
  67. package/dist/src/certification/operator-catalog.d.ts.map +1 -0
  68. package/dist/src/certification/operator-catalog.js +466 -0
  69. package/dist/src/certification/report.d.ts +4 -0
  70. package/dist/src/certification/report.d.ts.map +1 -0
  71. package/dist/src/certification/report.js +90 -0
  72. package/dist/src/certification/run.d.ts +28 -0
  73. package/dist/src/certification/run.d.ts.map +1 -0
  74. package/dist/src/certification/run.js +31 -0
  75. package/dist/src/certification/schema-index.d.ts +69 -0
  76. package/dist/src/certification/schema-index.d.ts.map +1 -0
  77. package/dist/src/certification/schema-index.js +82 -0
  78. package/dist/src/certification/target.d.ts +40 -0
  79. package/dist/src/certification/target.d.ts.map +1 -0
  80. package/dist/src/certification/target.js +167 -0
  81. package/dist/src/eval/cli.d.ts +3 -0
  82. package/dist/src/eval/cli.d.ts.map +1 -0
  83. package/dist/src/eval/cli.js +116 -0
  84. package/dist/src/eval/corpus.d.ts +27 -0
  85. package/dist/src/eval/corpus.d.ts.map +1 -0
  86. package/dist/src/eval/corpus.js +134 -0
  87. package/dist/src/eval/drivers/anthropic.d.ts +14 -0
  88. package/dist/src/eval/drivers/anthropic.d.ts.map +1 -0
  89. package/dist/src/eval/drivers/anthropic.js +120 -0
  90. package/dist/src/eval/drivers/bedrock.d.ts +54 -0
  91. package/dist/src/eval/drivers/bedrock.d.ts.map +1 -0
  92. package/dist/src/eval/drivers/bedrock.js +178 -0
  93. package/dist/src/eval/drivers/deterministic.d.ts +17 -0
  94. package/dist/src/eval/drivers/deterministic.d.ts.map +1 -0
  95. package/dist/src/eval/drivers/deterministic.js +51 -0
  96. package/dist/src/eval/drivers/index.d.ts +34 -0
  97. package/dist/src/eval/drivers/index.d.ts.map +1 -0
  98. package/dist/src/eval/drivers/index.js +74 -0
  99. package/dist/src/eval/drivers/openai.d.ts +14 -0
  100. package/dist/src/eval/drivers/openai.d.ts.map +1 -0
  101. package/dist/src/eval/drivers/openai.js +136 -0
  102. package/dist/src/eval/grade.d.ts +14 -0
  103. package/dist/src/eval/grade.d.ts.map +1 -0
  104. package/dist/src/eval/grade.js +56 -0
  105. package/dist/src/eval/northstar-corpus.d.ts +3 -0
  106. package/dist/src/eval/northstar-corpus.d.ts.map +1 -0
  107. package/dist/src/eval/northstar-corpus.js +194 -0
  108. package/dist/src/eval/operator-corpus.d.ts +3 -0
  109. package/dist/src/eval/operator-corpus.d.ts.map +1 -0
  110. package/dist/src/eval/operator-corpus.js +193 -0
  111. package/dist/src/eval/report.d.ts +104 -0
  112. package/dist/src/eval/report.d.ts.map +1 -0
  113. package/dist/src/eval/report.js +130 -0
  114. package/dist/src/eval/runner.d.ts +22 -0
  115. package/dist/src/eval/runner.d.ts.map +1 -0
  116. package/dist/src/eval/runner.js +116 -0
  117. package/dist/src/eval/types.d.ts +93 -0
  118. package/dist/src/eval/types.d.ts.map +1 -0
  119. package/dist/src/eval/types.js +10 -0
  120. package/dist/src/helpers.d.ts +11 -0
  121. package/dist/src/helpers.d.ts.map +1 -1
  122. package/dist/src/helpers.js +22 -0
  123. package/dist/src/index.d.ts +7 -0
  124. package/dist/src/index.d.ts.map +1 -1
  125. package/dist/src/index.js +30 -4
  126. package/dist/src/proxy.d.ts +46 -0
  127. package/dist/src/proxy.d.ts.map +1 -0
  128. package/dist/src/proxy.js +124 -0
  129. package/dist/src/resources/styles.d.ts +29 -0
  130. package/dist/src/resources/styles.d.ts.map +1 -0
  131. package/dist/src/resources/styles.js +43 -0
  132. package/dist/src/styles.d.ts +110 -0
  133. package/dist/src/styles.d.ts.map +1 -0
  134. package/dist/src/styles.js +118 -0
  135. package/dist/src/tools/apply-style-preset.d.ts +27 -0
  136. package/dist/src/tools/apply-style-preset.d.ts.map +1 -0
  137. package/dist/src/tools/apply-style-preset.js +29 -0
  138. package/dist/src/tools/count-features.d.ts.map +1 -1
  139. package/dist/src/tools/count-features.js +5 -4
  140. package/dist/src/tools/get-extent.d.ts.map +1 -1
  141. package/dist/src/tools/get-extent.js +6 -4
  142. package/dist/src/tools/get-style.d.ts +24 -0
  143. package/dist/src/tools/get-style.d.ts.map +1 -0
  144. package/dist/src/tools/get-style.js +28 -0
  145. package/dist/src/tools/query-features.d.ts.map +1 -1
  146. package/dist/src/tools/query-features.js +4 -1
  147. package/package.json +19 -4
@@ -0,0 +1,174 @@
1
+ import { McpError } from "@modelcontextprotocol/sdk/types.js";
2
+ import { validateAgainstSchema } from "./json-schema.js";
3
+ import { loadSchemaFile } from "./schema-index.js";
4
+ /**
5
+ * Pure, deterministic contract-check analyzers for MCP certification.
6
+ *
7
+ * These operate on values already obtained from the certified surface (tool
8
+ * results, thrown errors, paginated list responses) so they are unit-testable
9
+ * without any transport. The certifier does the I/O; these judge the shape.
10
+ *
11
+ * Contracts enforced (§P0.2):
12
+ * - output-schema : a tool's structuredContent validates against its outputSchema.
13
+ * - error-shape : invalid arguments yield an `isError` tool result carrying a
14
+ * structured, actionable code (validated against the vendored
15
+ * geoprocessing-error schema), never a protocol error.
16
+ * - auth : unauthenticated tools/call and resources/read surface a
17
+ * structured `unauthenticated` / `permission_denied` signal.
18
+ * - pagination : list `nextCursor` is honored and terminates.
19
+ */
20
+ /** Machine codes that satisfy the unauthenticated / permission-denied contract. */
21
+ export const AUTH_ERROR_CODES = ["unauthenticated", "permission_denied"];
22
+ /** geoprocessing-error `kind` values that denote an authorization failure. */
23
+ export const AUTH_ERROR_KINDS = ["AuthorizationDenied", "Unauthenticated", "PermissionDenied"];
24
+ let cachedErrorSchema;
25
+ function geoprocessingErrorSchema() {
26
+ if (!cachedErrorSchema) {
27
+ cachedErrorSchema = loadSchemaFile("common/geoprocessing-error.schema.json");
28
+ }
29
+ return cachedErrorSchema;
30
+ }
31
+ function isRecord(value) {
32
+ return typeof value === "object" && value !== null;
33
+ }
34
+ /**
35
+ * Normalize a structured error envelope out of any of the places a surface may
36
+ * carry it: a tool result's `structuredContent`, an `McpError`'s `data`, or a
37
+ * bare geoprocessing-error object. Returns undefined when no structure is found.
38
+ */
39
+ export function extractStructuredError(source) {
40
+ if (source instanceof McpError) {
41
+ const fromData = extractStructuredError(source.data);
42
+ if (fromData) {
43
+ return fromData;
44
+ }
45
+ // A raw code may live on the error's message-less data; nothing structured.
46
+ return undefined;
47
+ }
48
+ if (!isRecord(source)) {
49
+ return undefined;
50
+ }
51
+ // Tool result → dig into structuredContent first.
52
+ if (isRecord(source.structuredContent)) {
53
+ const nested = extractStructuredError(source.structuredContent);
54
+ if (nested) {
55
+ return nested;
56
+ }
57
+ }
58
+ const code = typeof source.code === "string" ? source.code : undefined;
59
+ // Envelope with a nested geoprocessing-error under `error`.
60
+ if (isRecord(source.error)) {
61
+ return { code, error: source.error };
62
+ }
63
+ // The object may itself be a geoprocessing-error (has `kind` + `message`).
64
+ if (typeof source.kind === "string" && typeof source.message === "string") {
65
+ return { code, error: source };
66
+ }
67
+ if (code) {
68
+ return { code };
69
+ }
70
+ return undefined;
71
+ }
72
+ /** Validate a value against the vendored geoprocessing-error schema. */
73
+ export function validateGeoprocessingError(error) {
74
+ const result = validateAgainstSchema(geoprocessingErrorSchema(), error);
75
+ return { ok: result.valid, errors: result.errors };
76
+ }
77
+ /**
78
+ * error-shape contract: an invalid-argument tool call must return an `isError`
79
+ * result whose structuredContent carries an actionable code and a
80
+ * geoprocessing-error-shaped `ValidationFailed` envelope.
81
+ */
82
+ export function checkInvalidArgsContract(result) {
83
+ const errors = [];
84
+ if (result.isError !== true) {
85
+ errors.push("expected isError=true for an invalid-argument call (got a protocol error or a success)");
86
+ }
87
+ const envelope = extractStructuredError(result);
88
+ if (!envelope) {
89
+ errors.push("no structured error envelope found on the tool result");
90
+ return { ok: false, errors };
91
+ }
92
+ if (!envelope.code || envelope.code.trim().length === 0) {
93
+ errors.push("structured error is missing a machine-actionable `code`");
94
+ }
95
+ if (envelope.error === undefined) {
96
+ errors.push("structured error is missing a geoprocessing-error envelope");
97
+ }
98
+ else {
99
+ const validation = validateGeoprocessingError(envelope.error);
100
+ if (!validation.ok) {
101
+ errors.push(...validation.errors.map((e) => `geoprocessing-error: ${e}`));
102
+ }
103
+ const kind = isRecord(envelope.error) ? envelope.error.kind : undefined;
104
+ if (kind !== "ValidationFailed") {
105
+ errors.push(`expected geoprocessing-error kind "ValidationFailed", got "${String(kind)}"`);
106
+ }
107
+ const violations = isRecord(envelope.error) ? envelope.error.violations : undefined;
108
+ if (!Array.isArray(violations) || violations.length === 0) {
109
+ errors.push("expected at least one structured validation `violation`");
110
+ }
111
+ }
112
+ return { ok: errors.length === 0, errors };
113
+ }
114
+ /**
115
+ * auth contract: an unauthenticated call must surface a structured
116
+ * `unauthenticated` / `permission_denied` signal — either as a code on the
117
+ * envelope or as an AuthorizationDenied geoprocessing-error kind.
118
+ */
119
+ export function checkAuthContract(source) {
120
+ const envelope = extractStructuredError(source);
121
+ if (!envelope) {
122
+ return { ok: false, errors: ["no structured error found for the unauthenticated request"] };
123
+ }
124
+ const code = envelope.code;
125
+ const kind = isRecord(envelope.error) ? envelope.error.kind : undefined;
126
+ const codeOk = typeof code === "string" && AUTH_ERROR_CODES.includes(code);
127
+ const kindOk = typeof kind === "string" && AUTH_ERROR_KINDS.includes(kind);
128
+ if (codeOk || kindOk) {
129
+ return { ok: true, errors: [] };
130
+ }
131
+ return {
132
+ ok: false,
133
+ errors: [
134
+ `expected a structured auth signal (code in [${AUTH_ERROR_CODES.join(", ")}] or kind in [${AUTH_ERROR_KINDS.join(", ")}]), got code="${String(code)}" kind="${String(kind)}"`,
135
+ ],
136
+ };
137
+ }
138
+ /**
139
+ * pagination contract: following `nextCursor` yields additional distinct items,
140
+ * never repeats a cursor (terminates), and the final page has no cursor.
141
+ */
142
+ export function checkPaginationContract(pages) {
143
+ const errors = [];
144
+ if (pages.length === 0) {
145
+ return { ok: false, errors: ["no list pages captured"] };
146
+ }
147
+ const seenCursors = new Set();
148
+ const seenKeys = new Set();
149
+ for (let i = 0; i < pages.length; i++) {
150
+ const page = pages[i];
151
+ for (const key of page.keys) {
152
+ seenKeys.add(key);
153
+ }
154
+ const isLast = i === pages.length - 1;
155
+ if (!isLast) {
156
+ if (!page.nextCursor) {
157
+ errors.push(`page ${i} is not the last page but advertised no nextCursor`);
158
+ }
159
+ else if (seenCursors.has(page.nextCursor)) {
160
+ errors.push(`nextCursor "${page.nextCursor}" repeated — pagination does not terminate`);
161
+ }
162
+ else {
163
+ seenCursors.add(page.nextCursor);
164
+ }
165
+ }
166
+ else if (page.nextCursor) {
167
+ errors.push(`final page still advertises nextCursor "${page.nextCursor}" — pagination did not terminate`);
168
+ }
169
+ }
170
+ if (pages.length > 1 && seenKeys.size <= pages[0].keys.length) {
171
+ errors.push("following nextCursor did not yield any additional distinct items");
172
+ }
173
+ return { ok: errors.length === 0, errors };
174
+ }
@@ -0,0 +1,4 @@
1
+ import type { HonuaClient } from "@honua/sdk-js";
2
+ /** Build an offline fixture client usable anywhere a `HonuaClient` is expected. */
3
+ export declare function createFixtureClient(): HonuaClient;
4
+ //# sourceMappingURL=fixture-client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fixture-client.d.ts","sourceRoot":"","sources":["../../../src/certification/fixture-client.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,WAAW,EAKZ,MAAM,eAAe,CAAC;AAsDvB,mFAAmF;AACnF,wBAAgB,mBAAmB,IAAI,WAAW,CAkEjD"}
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Offline fixture `HonuaClient` for deterministic certification.
3
+ *
4
+ * This is the standalone (non-vitest) counterpart of `test/test-helpers.ts`:
5
+ * it returns canned, schema-shaped responses for every method the MCP tools and
6
+ * resources call, so the certifier can run `tools/call` round-trips with no live
7
+ * backend, no network access, and no model/API calls. CI instead points the
8
+ * certifier at a live honua-server via `createClientFromEnv`.
9
+ */
10
+ const STYLES_LIST = {
11
+ styles: [
12
+ { id: "topographic", title: "Topographic", links: [] },
13
+ { id: "imagery", title: "Imagery", links: [] },
14
+ ],
15
+ default: "topographic",
16
+ };
17
+ const TOPOGRAPHIC_METADATA = {
18
+ id: "topographic",
19
+ title: "Topographic",
20
+ description: "Default topographic basemap style",
21
+ version: "3",
22
+ links: [{ rel: "preview", type: "image/png", href: "https://example.test/ogc/styles/topographic/preview" }],
23
+ };
24
+ const TOPOGRAPHIC_STYLESHEET = {
25
+ version: 8,
26
+ name: "Topographic",
27
+ layers: [{ id: "background", type: "background" }],
28
+ };
29
+ function jsonResponse(body, status = 200) {
30
+ return new Response(JSON.stringify(body), {
31
+ status,
32
+ headers: { "Content-Type": "application/json" },
33
+ });
34
+ }
35
+ async function fixtureFetch(_method, path) {
36
+ if (path === "/ogc/styles") {
37
+ return jsonResponse(STYLES_LIST);
38
+ }
39
+ if (path === "/ogc/styles/topographic/metadata") {
40
+ return jsonResponse(TOPOGRAPHIC_METADATA);
41
+ }
42
+ if (path === "/ogc/styles/topographic") {
43
+ return jsonResponse(TOPOGRAPHIC_STYLESHEET);
44
+ }
45
+ return jsonResponse({ error: "not found" }, 404);
46
+ }
47
+ /** Build an offline fixture client usable anywhere a `HonuaClient` is expected. */
48
+ export function createFixtureClient() {
49
+ const client = {
50
+ serverBaseUrl: "https://example.test",
51
+ async listServices() {
52
+ return {
53
+ services: [
54
+ { name: "Parks", type: "FeatureServer" },
55
+ { name: "Basemap", type: "MapServer" },
56
+ { name: "Census", type: "FeatureServer" },
57
+ ],
58
+ };
59
+ },
60
+ async getFeatureServiceMetadata(_serviceId) {
61
+ return {
62
+ serviceDescription: "Test service",
63
+ layers: [
64
+ { id: 0, name: "Layer 0" },
65
+ { id: 1, name: "Layer 1" },
66
+ ],
67
+ spatialReference: { wkid: 4326 },
68
+ };
69
+ },
70
+ async getLayerMetadata(_serviceId, _layerId) {
71
+ return {
72
+ id: 0,
73
+ name: "Test Layer",
74
+ description: "A test layer",
75
+ geometryType: "esriGeometryPoint",
76
+ fields: [
77
+ { name: "OBJECTID", type: "esriFieldTypeOID" },
78
+ { name: "NAME", type: "esriFieldTypeString", alias: "Feature Name" },
79
+ { name: "VALUE", type: "esriFieldTypeDouble" },
80
+ ],
81
+ extent: { xmin: -180, ymin: -90, xmax: 180, ymax: 90 },
82
+ spatialReference: { wkid: 4326 },
83
+ relationships: [{ id: 0, name: "rel_0", relatedTableId: 1 }],
84
+ };
85
+ },
86
+ async queryFeatures(request) {
87
+ const extraParams = request?.extraParams ?? {};
88
+ // count-only and extent-only projections mirror the GeoServices query
89
+ // surface the tools exercise (returnCountOnly / returnExtentOnly).
90
+ if (extraParams.returnCountOnly === true) {
91
+ return { count: 2 };
92
+ }
93
+ if (extraParams.returnExtentOnly === true) {
94
+ return {
95
+ count: 2,
96
+ extent: { xmin: -180, ymin: -90, xmax: 180, ymax: 90, spatialReference: { wkid: 4326 } },
97
+ };
98
+ }
99
+ return {
100
+ objectIdFieldName: "OBJECTID",
101
+ geometryType: "esriGeometryPoint",
102
+ spatialReference: { wkid: 4326 },
103
+ features: [
104
+ { attributes: { OBJECTID: 1, NAME: "Park A", VALUE: 100 } },
105
+ { attributes: { OBJECTID: 2, NAME: "Park B", VALUE: 200 } },
106
+ ],
107
+ exceededTransferLimit: false,
108
+ };
109
+ },
110
+ pipelineFetch: fixtureFetch,
111
+ };
112
+ return client;
113
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Deterministic round-trip fixture inputs for the honua `/mcp` operator surface.
3
+ *
4
+ * Certification calls `tools/call` on read-only operator tools to validate their
5
+ * structured output against the advertised outputSchema. Inputs are resolved
6
+ * from environment overrides so the same harness drives both the offline mock
7
+ * upstream and a live honua-server `/mcp` in remote mode, without ever calling a
8
+ * mutating tool (execute/cancel/publish/propose/create).
9
+ */
10
+ export interface RoundTripEnv {
11
+ serviceId: string;
12
+ layerId: number;
13
+ address: string;
14
+ styleId: string | undefined;
15
+ }
16
+ /** Resolve round-trip identifiers from the environment, with safe fallbacks. */
17
+ export declare function resolveRoundTripEnv(env?: NodeJS.ProcessEnv): RoundTripEnv;
18
+ /**
19
+ * Build the read-only round-trip argument map keyed by advertised tool name.
20
+ * Tools without a fixture entry are still discovered, schema/conformance/output
21
+ * checked, but are not round-tripped (no call is made). Mutating tools are never
22
+ * listed here.
23
+ */
24
+ export declare function buildRoundTripInputs(env: RoundTripEnv): Record<string, Record<string, unknown>>;
25
+ /**
26
+ * A deliberately invalid argument set for the error-shape contract. Certification
27
+ * picks the first advertised tool that has an entry here and calls it with these
28
+ * args, expecting a structured `ValidationFailed` tool result (not a protocol
29
+ * error). `honua_query_features` requires `layerId`, so omitting it is invalid.
30
+ */
31
+ export declare function buildInvalidArgsInputs(): Record<string, Record<string, unknown>>;
32
+ //# sourceMappingURL=fixtures.d.ts.map
@@ -0,0 +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"}
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Deterministic round-trip fixture inputs for the honua `/mcp` operator surface.
3
+ *
4
+ * Certification calls `tools/call` on read-only operator tools to validate their
5
+ * structured output against the advertised outputSchema. Inputs are resolved
6
+ * from environment overrides so the same harness drives both the offline mock
7
+ * upstream and a live honua-server `/mcp` in remote mode, without ever calling a
8
+ * mutating tool (execute/cancel/publish/propose/create).
9
+ */
10
+ /** Resolve round-trip identifiers from the environment, with safe fallbacks. */
11
+ export function resolveRoundTripEnv(env = process.env) {
12
+ const serviceId = env.HONUA_MCP_SERVICE_ID ?? env.HONUA_SERVICE_ID ?? "svc-parks";
13
+ const layerRaw = env.HONUA_MCP_LAYER_ID ?? env.HONUA_LAYER_ID ?? "0";
14
+ const parsedLayer = Number.parseInt(layerRaw, 10);
15
+ const layerId = Number.isFinite(parsedLayer) && parsedLayer >= 0 ? parsedLayer : 0;
16
+ const address = env.HONUA_MCP_ADDRESS ?? "1600 Pennsylvania Ave NW, Washington DC";
17
+ const styleId = env.HONUA_MCP_STYLE_ID ?? env.HONUA_STYLE_ID;
18
+ return { serviceId, layerId, address, styleId };
19
+ }
20
+ /**
21
+ * Build the read-only round-trip argument map keyed by advertised tool name.
22
+ * Tools without a fixture entry are still discovered, schema/conformance/output
23
+ * checked, but are not round-tripped (no call is made). Mutating tools are never
24
+ * listed here.
25
+ */
26
+ export function buildRoundTripInputs(env) {
27
+ const { serviceId, layerId, address } = env;
28
+ return {
29
+ honua_list_layers: {},
30
+ honua_list_capabilities: {},
31
+ honua_query_features: { serviceId, layerId, limit: 1 },
32
+ honua_render_map: { layers: [{ serviceId, layerId }], bbox: [-77.1, 38.8, -76.9, 39.0] },
33
+ honua_geocode_address: { address },
34
+ honua_solve_route: {
35
+ stops: [
36
+ { lon: -77.03, lat: 38.9 },
37
+ { lon: -76.61, lat: 39.29 },
38
+ ],
39
+ },
40
+ honua_plan_analysis: { intent: "Count parks within the district boundary" },
41
+ honua_ground_candidates: { goal: "Analyze park coverage" },
42
+ honua_clarify_intent: {
43
+ goal: "Analyze park coverage",
44
+ intentId: "int-001",
45
+ response: { answers: { q1: ["yes"] } },
46
+ },
47
+ honua_validate_plan: { plan: {} },
48
+ honua_dry_run_plan: { plan: { planId: "plan-001", steps: [{ stepId: "s1", kind: "QueryFeatures" }] } },
49
+ honua_resolve_entity: { text: "parks" },
50
+ };
51
+ }
52
+ /**
53
+ * A deliberately invalid argument set for the error-shape contract. Certification
54
+ * picks the first advertised tool that has an entry here and calls it with these
55
+ * args, expecting a structured `ValidationFailed` tool result (not a protocol
56
+ * error). `honua_query_features` requires `layerId`, so omitting it is invalid.
57
+ */
58
+ export function buildInvalidArgsInputs() {
59
+ return {
60
+ honua_query_features: { serviceId: "svc-parks" },
61
+ honua_geocode_address: { maxResults: 3 },
62
+ honua_solve_route: { travelProfile: "driving" },
63
+ };
64
+ }
@@ -0,0 +1,49 @@
1
+ import type { ValidateFunction } from "ajv/dist/2020.js";
2
+ import type { JsonSchema } from "./schema-index.js";
3
+ export interface WellFormedResult {
4
+ wellFormed: boolean;
5
+ errors: string[];
6
+ }
7
+ /**
8
+ * Check that a value is a structurally well-formed JSON Schema by asking Ajv to
9
+ * compile it. A schema that compiles is usable as a validator; a schema Ajv
10
+ * refuses to compile is malformed.
11
+ */
12
+ export declare function checkWellFormed(schema: unknown): WellFormedResult;
13
+ export interface ValidationResult {
14
+ valid: boolean;
15
+ errors: string[];
16
+ }
17
+ /** Compile a schema into a reusable validator, or throw a descriptive error. */
18
+ export declare function compileValidator(schema: JsonSchema): ValidateFunction;
19
+ /** Validate a value against a JSON Schema and collect human-readable errors. */
20
+ export declare function validateAgainstSchema(schema: JsonSchema, value: unknown): ValidationResult;
21
+ export interface ConformanceResult {
22
+ conformant: boolean;
23
+ /** Required standard properties the advertised schema honors. */
24
+ matchedRequired: string[];
25
+ /** Conformance violations (missing or type-incompatible required properties). */
26
+ violations: string[];
27
+ /** Non-fatal observations (e.g. advertised extra properties, type widenings). */
28
+ notes: string[];
29
+ }
30
+ /**
31
+ * Deterministically compare an advertised tool inputSchema against a standard
32
+ * geospatial-mcp tool schema.
33
+ *
34
+ * Conformance contract (intentionally permissive to avoid false negatives — we
35
+ * fail only on demonstrable non-conformance, never on the absence of a tool):
36
+ *
37
+ * 1. Every property the standard marks `required` MUST be present in the
38
+ * advertised schema's properties (a client that follows the standard will
39
+ * send it, so the tool must accept it).
40
+ * 2. For each such required property, when both schemas declare a `type`, the
41
+ * advertised type must be compatible (equal, or a superset, or `integer`
42
+ * accepted where the standard says `number`).
43
+ *
44
+ * Extra advertised properties and looser-but-compatible types are recorded as
45
+ * notes, not violations: a richer reference shape is allowed to extend the
46
+ * bare standard.
47
+ */
48
+ export declare function checkConformance(advertised: JsonSchema, standard: JsonSchema): ConformanceResult;
49
+ //# sourceMappingURL=json-schema.d.ts.map
@@ -0,0 +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"}
@@ -0,0 +1,164 @@
1
+ import { Ajv2020 } from "ajv/dist/2020.js";
2
+ import draft07MetaSchema from "ajv/dist/refs/json-schema-draft-07.json" with { type: "json" };
3
+ /**
4
+ * Deterministic JSON Schema utilities for certification.
5
+ *
6
+ * Two dialects appear in practice:
7
+ * - The vendored geospatial-mcp standard schemas declare draft 2020-12.
8
+ * - MCP tool `inputSchema`s emitted via zod-to-json-schema declare draft-07.
9
+ *
10
+ * A single Ajv 2020-12 core handles both: we register the draft-07 meta-schema
11
+ * so a schema that declares `$schema: ".../draft-07/schema#"` resolves instead
12
+ * of erroring with "no schema with key/ref". All work is in-process: no network
13
+ * access, no model calls.
14
+ *
15
+ * `strict: false` tolerates non-standard annotation keywords (e.g.
16
+ * `x-honua-reference-shape`); `validateFormats: false` keeps well-formedness
17
+ * purely structural and offline.
18
+ */
19
+ function createAjv() {
20
+ const ajv = new Ajv2020({ strict: false, allErrors: true, validateFormats: false });
21
+ ajv.addMetaSchema(draft07MetaSchema);
22
+ return ajv;
23
+ }
24
+ /**
25
+ * Check that a value is a structurally well-formed JSON Schema by asking Ajv to
26
+ * compile it. A schema that compiles is usable as a validator; a schema Ajv
27
+ * refuses to compile is malformed.
28
+ */
29
+ export function checkWellFormed(schema) {
30
+ if (schema === undefined || schema === null) {
31
+ return { wellFormed: false, errors: ["schema is missing (null/undefined)"] };
32
+ }
33
+ if (typeof schema !== "object") {
34
+ return { wellFormed: false, errors: [`schema must be an object, got ${typeof schema}`] };
35
+ }
36
+ try {
37
+ const ajv = createAjv();
38
+ ajv.compile(schema);
39
+ return { wellFormed: true, errors: [] };
40
+ }
41
+ catch (err) {
42
+ return { wellFormed: false, errors: [err instanceof Error ? err.message : String(err)] };
43
+ }
44
+ }
45
+ function formatErrors(errors) {
46
+ if (!errors) {
47
+ return [];
48
+ }
49
+ return errors.map((e) => `${e.instancePath || "(root)"} ${e.message ?? "is invalid"}`.trim());
50
+ }
51
+ /** Compile a schema into a reusable validator, or throw a descriptive error. */
52
+ export function compileValidator(schema) {
53
+ const ajv = createAjv();
54
+ return ajv.compile(schema);
55
+ }
56
+ /** Validate a value against a JSON Schema and collect human-readable errors. */
57
+ export function validateAgainstSchema(schema, value) {
58
+ try {
59
+ const validate = compileValidator(schema);
60
+ const valid = validate(value);
61
+ return { valid, errors: valid ? [] : formatErrors(validate.errors) };
62
+ }
63
+ catch (err) {
64
+ return { valid: false, errors: [err instanceof Error ? err.message : String(err)] };
65
+ }
66
+ }
67
+ /**
68
+ * Extract the top-level property names a JSON Schema accepts, when discoverable
69
+ * from a plain `type: object` shape. Returns undefined when the property set is
70
+ * not statically enumerable (e.g. additionalProperties / combinators only).
71
+ */
72
+ function objectPropertyNames(schema) {
73
+ const props = schema.properties;
74
+ if (props && typeof props === "object") {
75
+ return new Set(Object.keys(props));
76
+ }
77
+ return undefined;
78
+ }
79
+ function requiredPropertyNames(schema) {
80
+ const required = schema.required;
81
+ return Array.isArray(required) ? required.filter((r) => typeof r === "string") : [];
82
+ }
83
+ /** Normalize a JSON Schema `type` (string or array) into a set. */
84
+ function schemaTypes(schema) {
85
+ if (!schema) {
86
+ return new Set();
87
+ }
88
+ const t = schema.type;
89
+ if (typeof t === "string") {
90
+ return new Set([t]);
91
+ }
92
+ if (Array.isArray(t)) {
93
+ return new Set(t.filter((x) => typeof x === "string"));
94
+ }
95
+ return new Set();
96
+ }
97
+ /**
98
+ * Deterministically compare an advertised tool inputSchema against a standard
99
+ * geospatial-mcp tool schema.
100
+ *
101
+ * Conformance contract (intentionally permissive to avoid false negatives — we
102
+ * fail only on demonstrable non-conformance, never on the absence of a tool):
103
+ *
104
+ * 1. Every property the standard marks `required` MUST be present in the
105
+ * advertised schema's properties (a client that follows the standard will
106
+ * send it, so the tool must accept it).
107
+ * 2. For each such required property, when both schemas declare a `type`, the
108
+ * advertised type must be compatible (equal, or a superset, or `integer`
109
+ * accepted where the standard says `number`).
110
+ *
111
+ * Extra advertised properties and looser-but-compatible types are recorded as
112
+ * notes, not violations: a richer reference shape is allowed to extend the
113
+ * bare standard.
114
+ */
115
+ export function checkConformance(advertised, standard) {
116
+ const violations = [];
117
+ const notes = [];
118
+ const matchedRequired = [];
119
+ const standardRequired = requiredPropertyNames(standard);
120
+ const standardProps = (standard.properties ?? {});
121
+ const advertisedProps = (advertised.properties ?? {});
122
+ const advertisedNames = objectPropertyNames(advertised);
123
+ for (const name of standardRequired) {
124
+ const present = advertisedNames ? advertisedNames.has(name) : name in advertisedProps;
125
+ if (!present) {
126
+ violations.push(`missing required standard property "${name}"`);
127
+ continue;
128
+ }
129
+ const standardTypes = schemaTypes(standardProps[name]);
130
+ const advertisedTypes = schemaTypes(advertisedProps[name]);
131
+ if (standardTypes.size > 0 && advertisedTypes.size > 0) {
132
+ const compatible = [...standardTypes].every((st) => {
133
+ if (advertisedTypes.has(st)) {
134
+ return true;
135
+ }
136
+ // `integer` is a refinement of `number`: a standard `number` is
137
+ // satisfied by an advertised `integer`, and vice versa is acceptable.
138
+ if (st === "number" && advertisedTypes.has("integer")) {
139
+ return true;
140
+ }
141
+ if (st === "integer" && advertisedTypes.has("number")) {
142
+ notes.push(`property "${name}" widens standard integer to number`);
143
+ return true;
144
+ }
145
+ return false;
146
+ });
147
+ if (!compatible) {
148
+ violations.push(`property "${name}" type ${[...advertisedTypes].join("|")} is incompatible with standard ${[...standardTypes].join("|")}`);
149
+ continue;
150
+ }
151
+ }
152
+ matchedRequired.push(name);
153
+ }
154
+ // Record advertised-only properties as informational coverage notes.
155
+ if (advertisedNames) {
156
+ const standardNames = new Set(Object.keys(standardProps));
157
+ for (const name of advertisedNames) {
158
+ if (!standardNames.has(name)) {
159
+ notes.push(`advertises non-standard property "${name}"`);
160
+ }
161
+ }
162
+ }
163
+ return { conformant: violations.length === 0, matchedRequired, violations, notes };
164
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * A real, in-process streamable-HTTP mock of the honua `/mcp` operator surface.
3
+ *
4
+ * The certification harness connects to this over the SAME client transport it
5
+ * uses for a live server (`StreamableHTTPClientTransport`), so the offline CI
6
+ * path exercises the identical connect/negotiate/tools-call code as the remote
7
+ * path — only the upstream is swapped for a deterministic fixture.
8
+ *
9
+ * Auth is enforced per request: the listener parses the bearer token / API key
10
+ * and attaches `req.auth` when present. Requests without a credential reach the
11
+ * operator server as unauthenticated, so the certifier's auth-contract pass sees
12
+ * the same structured `unauthenticated` signal a real deployment would emit.
13
+ */
14
+ export interface MockUpstreamOptions {
15
+ /** Bearer token the mock accepts; default `mock-cert-token`. */
16
+ authToken?: string;
17
+ /** Whether tools/list and resources/list paginate (default true). */
18
+ paginate?: boolean;
19
+ }
20
+ export interface MockUpstream {
21
+ /** Absolute URL of the mock `/mcp` endpoint (loopback, ephemeral port). */
22
+ url: string;
23
+ /** Bearer token the mock accepts. */
24
+ authToken: string;
25
+ close(): Promise<void>;
26
+ }
27
+ /** Start the mock operator upstream and resolve once it is listening. */
28
+ export declare function startMockUpstream(options?: MockUpstreamOptions): Promise<MockUpstream>;
29
+ //# sourceMappingURL=mock-upstream.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mock-upstream.d.ts","sourceRoot":"","sources":["../../../src/certification/mock-upstream.ts"],"names":[],"mappings":"AAQA;;;;;;;;;;;;GAYG;AAEH,MAAM,WAAW,mBAAmB;IAClC,gEAAgE;IAChE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,qEAAqE;IACrE,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,YAAY;IAC3B,2EAA2E;IAC3E,GAAG,EAAE,MAAM,CAAC;IACZ,qCAAqC;IACrC,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AAcD,yEAAyE;AACzE,wBAAsB,iBAAiB,CAAC,OAAO,GAAE,mBAAwB,GAAG,OAAO,CAAC,YAAY,CAAC,CA4BhG"}