@honua/mcp-server 0.1.2-beta.0 → 0.1.8-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 (117) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +232 -9
  3. package/dist/src/certification/admin-parity.d.ts +96 -0
  4. package/dist/src/certification/admin-parity.d.ts.map +1 -0
  5. package/dist/src/certification/admin-parity.js +235 -0
  6. package/dist/src/certification/census-fixture-client.d.ts.map +1 -1
  7. package/dist/src/certification/census-fixture-client.js +5 -0
  8. package/dist/src/certification/certifier.d.ts +1 -1
  9. package/dist/src/certification/certifier.d.ts.map +1 -1
  10. package/dist/src/certification/certifier.js +28 -20
  11. package/dist/src/certification/cli.js +1 -1
  12. package/dist/src/certification/cql2.d.ts +24 -0
  13. package/dist/src/certification/cql2.d.ts.map +1 -0
  14. package/dist/src/certification/cql2.js +354 -0
  15. package/dist/src/certification/fixtures.d.ts +8 -0
  16. package/dist/src/certification/fixtures.d.ts.map +1 -1
  17. package/dist/src/certification/fixtures.js +26 -10
  18. package/dist/src/certification/ogc-data.d.ts +30 -0
  19. package/dist/src/certification/ogc-data.d.ts.map +1 -0
  20. package/dist/src/certification/ogc-data.js +726 -0
  21. package/dist/src/certification/ogc-fixture-client.d.ts +4 -0
  22. package/dist/src/certification/ogc-fixture-client.d.ts.map +1 -0
  23. package/dist/src/certification/ogc-fixture-client.js +190 -0
  24. package/dist/src/certification/target.d.ts +10 -1
  25. package/dist/src/certification/target.d.ts.map +1 -1
  26. package/dist/src/certification/target.js +63 -28
  27. package/dist/src/credential-endpoint.d.ts +3 -0
  28. package/dist/src/credential-endpoint.d.ts.map +1 -0
  29. package/dist/src/credential-endpoint.js +24 -0
  30. package/dist/src/eval/cli.js +6 -3
  31. package/dist/src/eval/corpus.d.ts +7 -0
  32. package/dist/src/eval/corpus.d.ts.map +1 -1
  33. package/dist/src/eval/corpus.js +12 -0
  34. package/dist/src/eval/ogc-corpus.d.ts +3 -0
  35. package/dist/src/eval/ogc-corpus.d.ts.map +1 -0
  36. package/dist/src/eval/ogc-corpus.js +437 -0
  37. package/dist/src/eval/runner.d.ts +6 -0
  38. package/dist/src/eval/runner.d.ts.map +1 -1
  39. package/dist/src/eval/runner.js +21 -7
  40. package/dist/src/helpers.d.ts +8 -13
  41. package/dist/src/helpers.d.ts.map +1 -1
  42. package/dist/src/helpers.js +7 -35
  43. package/dist/src/index.d.ts +36 -8
  44. package/dist/src/index.d.ts.map +1 -1
  45. package/dist/src/index.js +55 -18
  46. package/dist/src/neutral/errors.d.ts +73 -0
  47. package/dist/src/neutral/errors.d.ts.map +1 -0
  48. package/dist/src/neutral/errors.js +112 -0
  49. package/dist/src/neutral/filter.d.ts +108 -0
  50. package/dist/src/neutral/filter.d.ts.map +1 -0
  51. package/dist/src/neutral/filter.js +271 -0
  52. package/dist/src/neutral/query.d.ts +101 -0
  53. package/dist/src/neutral/query.d.ts.map +1 -0
  54. package/dist/src/neutral/query.js +180 -0
  55. package/dist/src/neutral/result.d.ts +33 -0
  56. package/dist/src/neutral/result.d.ts.map +1 -0
  57. package/dist/src/neutral/result.js +38 -0
  58. package/dist/src/neutral/source-ref.d.ts +126 -0
  59. package/dist/src/neutral/source-ref.d.ts.map +1 -0
  60. package/dist/src/neutral/source-ref.js +267 -0
  61. package/dist/src/nl-map-control.d.ts.map +1 -1
  62. package/dist/src/proxy.d.ts.map +1 -1
  63. package/dist/src/proxy.js +29 -14
  64. package/dist/src/release/zero-to-map-checkpoint.d.ts +34 -0
  65. package/dist/src/release/zero-to-map-checkpoint.d.ts.map +1 -0
  66. package/dist/src/release/zero-to-map-checkpoint.js +369 -0
  67. package/dist/src/release/zero-to-map-cli.d.ts +51 -0
  68. package/dist/src/release/zero-to-map-cli.d.ts.map +1 -0
  69. package/dist/src/release/zero-to-map-cli.js +880 -0
  70. package/dist/src/release/zero-to-map-provision.d.ts +37 -0
  71. package/dist/src/release/zero-to-map-provision.d.ts.map +1 -0
  72. package/dist/src/release/zero-to-map-provision.js +177 -0
  73. package/dist/src/release/zero-to-map.d.ts +296 -0
  74. package/dist/src/release/zero-to-map.d.ts.map +1 -0
  75. package/dist/src/release/zero-to-map.js +1069 -0
  76. package/dist/src/tools/admin-install-local.d.ts +30 -0
  77. package/dist/src/tools/admin-install-local.d.ts.map +1 -0
  78. package/dist/src/tools/admin-install-local.js +22 -0
  79. package/dist/src/tools/control-plane-command.d.ts +99 -0
  80. package/dist/src/tools/control-plane-command.d.ts.map +1 -0
  81. package/dist/src/tools/control-plane-command.js +180 -0
  82. package/dist/src/tools/count-features.d.ts +85 -11
  83. package/dist/src/tools/count-features.d.ts.map +1 -1
  84. package/dist/src/tools/count-features.js +49 -31
  85. package/dist/src/tools/describe-layer.d.ts +30 -6
  86. package/dist/src/tools/describe-layer.d.ts.map +1 -1
  87. package/dist/src/tools/describe-layer.js +99 -8
  88. package/dist/src/tools/docs-search.d.ts +100 -0
  89. package/dist/src/tools/docs-search.d.ts.map +1 -0
  90. package/dist/src/tools/docs-search.js +381 -0
  91. package/dist/src/tools/get-extent.d.ts +69 -11
  92. package/dist/src/tools/get-extent.d.ts.map +1 -1
  93. package/dist/src/tools/get-extent.js +28 -46
  94. package/dist/src/tools/list-services.d.ts +7 -0
  95. package/dist/src/tools/list-services.d.ts.map +1 -1
  96. package/dist/src/tools/list-services.js +8 -1
  97. package/dist/src/tools/list-sources.d.ts +44 -0
  98. package/dist/src/tools/list-sources.d.ts.map +1 -0
  99. package/dist/src/tools/list-sources.js +138 -0
  100. package/dist/src/tools/query-features.d.ts +99 -19
  101. package/dist/src/tools/query-features.d.ts.map +1 -1
  102. package/dist/src/tools/query-features.js +38 -45
  103. package/dist/src/tools/statistics.d.ts +80 -12
  104. package/dist/src/tools/statistics.d.ts.map +1 -1
  105. package/dist/src/tools/statistics.js +56 -23
  106. package/package.json +37 -8
  107. package/release/zero-to-map/README.md +174 -0
  108. package/release/zero-to-map/configs/claude-code.mcp.json +11 -0
  109. package/release/zero-to-map/configs/claude-desktop.json +11 -0
  110. package/release/zero-to-map/configs/cursor.json +11 -0
  111. package/release/zero-to-map/contracts/aws-ecs-provision-binding.schema.json +59 -0
  112. package/release/zero-to-map/contracts/checkpoint.schema.json +152 -0
  113. package/release/zero-to-map/contracts/console-receipt.schema.json +150 -0
  114. package/release/zero-to-map/contracts/esri-gp-mcp.v1.json +73 -0
  115. package/release/zero-to-map/fixtures/parcels.geojson +33 -0
  116. package/release/zero-to-map/fixtures/zoning.geojson +24 -0
  117. package/release/zero-to-map/journey.v1.json +1248 -0
@@ -0,0 +1,4 @@
1
+ import type { HonuaClient } from "@honua/sdk-js";
2
+ /** Build the offline platform-free OGC API Features fixture client. */
3
+ export declare function createOgcFixtureClient(): HonuaClient;
4
+ //# sourceMappingURL=ogc-fixture-client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ogc-fixture-client.d.ts","sourceRoot":"","sources":["../../../src/certification/ogc-fixture-client.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AA2HjD,uEAAuE;AACvE,wBAAgB,sBAAsB,IAAI,WAAW,CAgGpD"}
@@ -0,0 +1,190 @@
1
+ import { compileCql2 } from "./cql2.js";
2
+ import { OGC_COLLECTIONS, OGC_COLLECTION_IDS, OGC_ENDPOINT } from "./ogc-data.js";
3
+ /**
4
+ * Offline NON-GeoServices fixture `HonuaClient` (issue #1005).
5
+ *
6
+ * Where `census-fixture-client.ts` models a plain Esri FeatureServer, this one
7
+ * models a plain **OGC API Features** endpoint — no `/rest/services`, no
8
+ * FeatureServer query surface, no Honua surfaces — replaying the recorded
9
+ * pygeoapi demo collections (`ogc-data.ts`) through an in-process evaluator that
10
+ * honours the OGC API Features Part 1 query parameters (`limit`, `offset`,
11
+ * `bbox`, `datetime`, `properties`, `sortby`) plus the Part 3 `filter` +
12
+ * `filter-lang=cql2-text` the neutral tool contract compiles to.
13
+ *
14
+ * Its whole purpose is to prove the protocol-neutral tool contract on a
15
+ * protocol whose vocabulary shares nothing with GeoServices: the GeoServices
16
+ * entry points all reject, so a tool that still secretly depended on
17
+ * `serviceId`/`layerId` fails loudly here instead of passing by accident.
18
+ */
19
+ const NO_GEOSERVICES = "This endpoint is OGC API Features only; it publishes no GeoServices/FeatureServer surface.";
20
+ function requireCollection(collectionId) {
21
+ const collection = OGC_COLLECTIONS[String(collectionId)];
22
+ if (!collection) {
23
+ throw new Error(`OGC API Features fixture: unknown collection "${String(collectionId)}"`);
24
+ }
25
+ return collection;
26
+ }
27
+ function geometryBbox(geometry) {
28
+ if (!geometry)
29
+ return undefined;
30
+ const coordinates = geometry.coordinates;
31
+ const xs = [];
32
+ const ys = [];
33
+ const visit = (node) => {
34
+ if (!Array.isArray(node))
35
+ return;
36
+ if (typeof node[0] === "number" && typeof node[1] === "number") {
37
+ xs.push(node[0]);
38
+ ys.push(node[1]);
39
+ return;
40
+ }
41
+ for (const child of node)
42
+ visit(child);
43
+ };
44
+ visit(coordinates);
45
+ if (xs.length === 0)
46
+ return undefined;
47
+ return [Math.min(...xs), Math.min(...ys), Math.max(...xs), Math.max(...ys)];
48
+ }
49
+ function bboxIntersects(feature, bbox) {
50
+ const [minX, minY, maxX, maxY] = bbox.split(",").map(Number);
51
+ const box = geometryBbox(feature.geometry);
52
+ if (!box)
53
+ return false;
54
+ return !(box[2] < minX || box[0] > maxX || box[3] < minY || box[1] > maxY);
55
+ }
56
+ /** The collection's own time dimension, when it publishes one. */
57
+ function timeField(collectionId) {
58
+ const properties = OGC_COLLECTIONS[collectionId]?.queryables ?? {};
59
+ return "datetime" in properties ? "datetime" : undefined;
60
+ }
61
+ function withinDatetime(feature, datetime, field) {
62
+ const raw = feature.properties?.[field];
63
+ if (typeof raw !== "string")
64
+ return false;
65
+ const value = Date.parse(raw);
66
+ if (Number.isNaN(value))
67
+ return false;
68
+ const [rawStart, rawEnd] = datetime.includes("/") ? datetime.split("/") : [datetime, datetime];
69
+ const start = rawStart === ".." || rawStart === "" ? Number.NEGATIVE_INFINITY : Date.parse(rawStart);
70
+ const end = rawEnd === ".." || rawEnd === "" ? Number.POSITIVE_INFINITY : Date.parse(rawEnd);
71
+ return value >= start && value <= end;
72
+ }
73
+ function sortFeatures(features, sortby) {
74
+ const specs = sortby
75
+ .split(",")
76
+ .map((part) => part.trim())
77
+ .filter(Boolean)
78
+ .map((part) => part.startsWith("-") ? { field: part.slice(1), descending: true } : { field: part, descending: false });
79
+ return [...features].sort((a, b) => {
80
+ for (const spec of specs) {
81
+ const av = a.properties?.[spec.field];
82
+ const bv = b.properties?.[spec.field];
83
+ if (av === bv)
84
+ continue;
85
+ const less = typeof av === "number" && typeof bv === "number" ? av < bv : String(av) < String(bv);
86
+ return (less ? -1 : 1) * (spec.descending ? -1 : 1);
87
+ }
88
+ return 0;
89
+ });
90
+ }
91
+ function projectProperties(feature, properties) {
92
+ if (properties === undefined)
93
+ return feature;
94
+ const requested = (Array.isArray(properties) ? properties : String(properties).split(","))
95
+ .map((name) => name.trim())
96
+ .filter(Boolean);
97
+ if (requested.length === 0 || requested.includes("*"))
98
+ return feature;
99
+ const projected = {};
100
+ for (const name of requested) {
101
+ if (feature.properties && name in feature.properties)
102
+ projected[name] = feature.properties[name];
103
+ }
104
+ return { ...feature, properties: projected };
105
+ }
106
+ /** Build the offline platform-free OGC API Features fixture client. */
107
+ export function createOgcFixtureClient() {
108
+ const client = {
109
+ serverBaseUrl: OGC_ENDPOINT,
110
+ async listOgcCollections(_request = {}) {
111
+ return {
112
+ collections: OGC_COLLECTION_IDS.map((id) => OGC_COLLECTIONS[id].collection),
113
+ };
114
+ },
115
+ async getOgcCollection(request) {
116
+ return requireCollection(request.collectionId).collection;
117
+ },
118
+ async getOgcQueryables(request) {
119
+ const collection = requireCollection(request.collectionId);
120
+ if (!collection.queryables) {
121
+ throw new Error(`OGC API Features fixture: collection "${String(request.collectionId)}" publishes no queryables`);
122
+ }
123
+ return { type: "object", properties: collection.queryables };
124
+ },
125
+ async listOgcItems(request) {
126
+ const collectionId = String(request.collectionId);
127
+ const collection = requireCollection(collectionId);
128
+ let features = collection.features;
129
+ if (request.filter !== undefined && request.filter !== "") {
130
+ if (request.filterLang !== undefined && request.filterLang !== "cql2-text") {
131
+ throw new Error(`OGC API Features fixture: unsupported filter-lang "${String(request.filterLang)}"`);
132
+ }
133
+ const predicate = compileCql2(request.filter);
134
+ features = features.filter((feature) => predicate(feature.properties ?? {}));
135
+ }
136
+ if (request.bbox !== undefined) {
137
+ features = features.filter((feature) => bboxIntersects(feature, request.bbox));
138
+ }
139
+ if (request.datetime !== undefined) {
140
+ const field = timeField(collectionId);
141
+ if (!field) {
142
+ throw new Error(`OGC API Features fixture: collection "${collectionId}" has no time dimension`);
143
+ }
144
+ features = features.filter((feature) => withinDatetime(feature, request.datetime, field));
145
+ }
146
+ if (request.ids !== undefined) {
147
+ const ids = (Array.isArray(request.ids) ? request.ids : String(request.ids).split(",")).map(String);
148
+ features = features.filter((feature) => ids.includes(String(feature.id)));
149
+ }
150
+ const numberMatched = features.length;
151
+ if (request.sortby)
152
+ features = sortFeatures(features, request.sortby);
153
+ const offset = request.offset ?? 0;
154
+ // OGC API Features servers apply a default and a maximum page size
155
+ // (Part 1 §7.15.4). The default mirrors the pygeoapi demo's 10; the
156
+ // maximum is generous so a client that asks to drain a collection
157
+ // actually drains it, rather than silently aggregating one page.
158
+ const limit = Math.min(request.limit ?? 10, 1000);
159
+ const page = features
160
+ .slice(offset, offset + limit)
161
+ .map((feature) => projectProperties(feature, request.properties));
162
+ return {
163
+ type: "FeatureCollection",
164
+ features: page,
165
+ numberMatched,
166
+ numberReturned: page.length,
167
+ };
168
+ },
169
+ // ── Surfaces this endpoint does NOT publish ──────────────────
170
+ async listServices() {
171
+ throw new Error(NO_GEOSERVICES);
172
+ },
173
+ async getFeatureServiceMetadata() {
174
+ throw new Error(NO_GEOSERVICES);
175
+ },
176
+ async getLayerMetadata() {
177
+ throw new Error(NO_GEOSERVICES);
178
+ },
179
+ async queryFeatures() {
180
+ throw new Error(NO_GEOSERVICES);
181
+ },
182
+ async pipelineFetch(_method, _path) {
183
+ return new Response(JSON.stringify({ error: { code: 404, message: "not found" } }), {
184
+ status: 404,
185
+ headers: { "Content-Type": "application/json" },
186
+ });
187
+ },
188
+ };
189
+ return client;
190
+ }
@@ -20,8 +20,17 @@ import { type ProxyOptions } from "../proxy.js";
20
20
  * of a PLAIN public FeatureServer (recorded census layer, no
21
21
  * Honua surfaces). Proves the tools certify green against a
22
22
  * non-Honua endpoint, with Honua-only tools skip-with-reason.
23
+ * - `standalone-ogc` : the same surface against a NON-GeoServices endpoint
24
+ * (issue #1005) — an in-process fixture of a plain OGC API
25
+ * Features server replaying recorded pygeoapi collections.
26
+ * Nothing Esri-shaped exists there (the GeoServices entry
27
+ * points all reject), so it proves the tool contract is
28
+ * genuinely protocol-neutral rather than GeoServices with
29
+ * extra fields.
23
30
  */
24
- export type CertTargetMode = "offline" | "remote" | "stdio-proxy" | "standalone";
31
+ export type CertTargetMode = "offline" | "remote" | "stdio-proxy" | "standalone" | "standalone-ogc";
32
+ /** Target modes that certify the platform-free `createServer` surface. */
33
+ export declare function isStandaloneTargetMode(mode: CertTargetMode): boolean;
25
34
  export interface CertificationTarget {
26
35
  mode: CertTargetMode;
27
36
  backend: "mock-upstream" | "live";
@@ -1 +1 @@
1
- {"version":3,"file":"target.d.ts","sourceRoot":"","sources":["../../../src/certification/target.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAC;AAInE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AACjD,OAAO,EAAE,KAAK,YAAY,EAAmB,MAAM,aAAa,CAAC;AAIjE;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,MAAM,MAAM,cAAc,GAAG,SAAS,GAAG,QAAQ,GAAG,aAAa,GAAG,YAAY,CAAC;AAEjF,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,cAAc,CAAC;IACrB,OAAO,EAAE,eAAe,GAAG,MAAM,CAAC;IAClC,4EAA4E;IAC5E,WAAW,EAAE,MAAM,CAAC;IACpB,iFAAiF;IACjF,QAAQ,EAAE,QAAQ,CAAC;IACnB,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,2CAA2C;IAC3C,MAAM,EAAE,MAAM,CAAC;IACf,yEAAyE;IACzE,2BAA2B,EAAE,OAAO,CAAC;IACrC,gFAAgF;IAChF,sBAAsB,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;IAC1C,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AAED,wBAAgB,iBAAiB,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,cAAc,CAQtF;AAED,6EAA6E;AAC7E,wBAAgB,yBAAyB,CAAC,GAAG,EAAE,MAAM,CAAC,UAAU,GAAG,YAAY,CAc9E;AAED,kFAAkF;AAClF,wBAAgB,iBAAiB,IAAI,MAAM,CAc1C;AAED,gDAAgD;AAChD,wBAAsB,uBAAuB,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAYhH"}
1
+ {"version":3,"file":"target.d.ts","sourceRoot":"","sources":["../../../src/certification/target.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAC;AAInE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AACjD,OAAO,EAAE,KAAK,YAAY,EAAwC,MAAM,aAAa,CAAC;AAMtF;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,MAAM,MAAM,cAAc,GAAG,SAAS,GAAG,QAAQ,GAAG,aAAa,GAAG,YAAY,GAAG,gBAAgB,CAAC;AAEpG,0EAA0E;AAC1E,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,cAAc,GAAG,OAAO,CAEpE;AAED,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,cAAc,CAAC;IACrB,OAAO,EAAE,eAAe,GAAG,MAAM,CAAC;IAClC,4EAA4E;IAC5E,WAAW,EAAE,MAAM,CAAC;IACpB,iFAAiF;IACjF,QAAQ,EAAE,QAAQ,CAAC;IACnB,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,2CAA2C;IAC3C,MAAM,EAAE,MAAM,CAAC;IACf,yEAAyE;IACzE,2BAA2B,EAAE,OAAO,CAAC;IACrC,gFAAgF;IAChF,sBAAsB,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;IAC1C,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AAED,wBAAgB,iBAAiB,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,cAAc,CActF;AAED,6EAA6E;AAC7E,wBAAgB,yBAAyB,CAAC,GAAG,EAAE,MAAM,CAAC,UAAU,GAAG,YAAY,CAK9E;AAED,kFAAkF;AAClF,wBAAgB,iBAAiB,IAAI,MAAM,CAc1C;AAED,gDAAgD;AAChD,wBAAsB,uBAAuB,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAehH"}
@@ -4,31 +4,32 @@ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
4
4
  import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
5
5
  import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
6
6
  import { createClientFromEnv, createServer } from "../index.js";
7
- import { connectUpstream } from "../proxy.js";
7
+ import { connectUpstream, resolveProxyOptions } from "../proxy.js";
8
8
  import { createStandaloneFixtureClient } from "./census-fixture-client.js";
9
9
  import { startMockUpstream } from "./mock-upstream.js";
10
+ import { OGC_ENDPOINT } from "./ogc-data.js";
11
+ import { createOgcFixtureClient } from "./ogc-fixture-client.js";
12
+ /** Target modes that certify the platform-free `createServer` surface. */
13
+ export function isStandaloneTargetMode(mode) {
14
+ return mode === "standalone" || mode === "standalone-ogc";
15
+ }
10
16
  export function resolveTargetMode(env = process.env) {
11
17
  const raw = (env.HONUA_MCP_CERT_TARGET ?? "offline").trim().toLowerCase();
12
- if (raw === "offline" || raw === "remote" || raw === "stdio-proxy" || raw === "standalone") {
18
+ if (raw === "offline" ||
19
+ raw === "remote" ||
20
+ raw === "stdio-proxy" ||
21
+ raw === "standalone" ||
22
+ raw === "standalone-ogc") {
13
23
  return raw;
14
24
  }
15
- throw new Error(`HONUA_MCP_CERT_TARGET must be "offline", "remote", "stdio-proxy", or "standalone", received "${raw}"`);
25
+ throw new Error(`HONUA_MCP_CERT_TARGET must be "offline", "remote", "stdio-proxy", "standalone", or "standalone-ogc", received "${raw}"`);
16
26
  }
17
27
  /** Resolve the remote `/mcp` endpoint + credentials from the environment. */
18
28
  export function resolveRemoteProxyOptions(env) {
19
- const remoteUrl = env.HONUA_MCP_REMOTE_URL ?? env.HONUA_MCP_URL;
20
- if (!remoteUrl) {
29
+ if (!(env.HONUA_MCP_REMOTE_URL ?? env.HONUA_MCP_URL)) {
21
30
  throw new Error("HONUA_MCP_REMOTE_URL is required for remote / stdio-proxy certification against a live /mcp.");
22
31
  }
23
- const parsed = new URL(remoteUrl);
24
- if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
25
- throw new Error(`HONUA_MCP_REMOTE_URL must use http or https: ${remoteUrl}`);
26
- }
27
- return {
28
- remoteUrl: parsed.toString(),
29
- authToken: env.HONUA_MCP_AUTH_TOKEN,
30
- apiKey: env.HONUA_API_KEY,
31
- };
32
+ return resolveProxyOptions(env);
32
33
  }
33
34
  /** Resolve the built `honua-mcp-proxy` entry usable from src (vitest) or dist. */
34
35
  export function resolveProxyEntry() {
@@ -56,8 +57,41 @@ export async function openCertificationTarget(env = process.env) {
56
57
  if (mode === "standalone") {
57
58
  return openStandaloneTarget(env);
58
59
  }
60
+ if (mode === "standalone-ogc") {
61
+ return openStandaloneOgcTarget(env);
62
+ }
59
63
  return openOfflineTarget(env);
60
64
  }
65
+ /**
66
+ * Open the NON-GeoServices standalone target (#1005): the same platform-free
67
+ * `createServer` surface, wired to an in-process fixture of a plain OGC API
68
+ * Features endpoint (recorded pygeoapi collections). The GeoServices entry
69
+ * points on that fixture all reject, so any tool still requiring Esri
70
+ * addressing fails here rather than passing by accident.
71
+ */
72
+ async function openStandaloneOgcTarget(env) {
73
+ const server = createServer(createOgcFixtureClient());
74
+ const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
75
+ const client = new Client({ name: "honua-mcp-certifier", version: "1.0.0" });
76
+ await server.connect(serverTransport);
77
+ await client.connect(clientTransport);
78
+ return {
79
+ mode: "standalone-ogc",
80
+ backend: "mock-upstream",
81
+ serverLabel: `honua-mcp standalone → ${OGC_ENDPOINT} (recorded OGC API Features fixture, no GeoServices surface)`,
82
+ authMode: "anonymous",
83
+ honuaTransport: env.HONUA_TRANSPORT ?? null,
84
+ client,
85
+ supportsUnauthenticatedPass: false,
86
+ connectUnauthenticated() {
87
+ return Promise.reject(new Error("standalone-ogc target has no auth boundary"));
88
+ },
89
+ async close() {
90
+ await client.close().catch(() => { });
91
+ await server.close().catch(() => { });
92
+ },
93
+ };
94
+ }
61
95
  /**
62
96
  * Open the platform-free standalone target: the direct-SDK `createServer` surface,
63
97
  * reached over an in-memory MCP transport. By default it is wired to an in-process
@@ -157,32 +191,33 @@ async function openStdioProxyTarget(env) {
157
191
  const clients = [];
158
192
  // Upstream: a live /mcp when configured, otherwise the in-process mock.
159
193
  let mock;
160
- let upstreamUrl;
161
- let upstreamToken;
194
+ let upstreamOptions;
162
195
  let backend;
163
196
  if (env.HONUA_MCP_REMOTE_URL ?? env.HONUA_MCP_URL) {
164
- const options = resolveRemoteProxyOptions(env);
165
- upstreamUrl = options.remoteUrl;
166
- upstreamToken = options.authToken;
197
+ upstreamOptions = resolveRemoteProxyOptions(env);
167
198
  backend = "live";
168
199
  }
169
200
  else {
170
201
  mock = await startMockUpstream();
171
- upstreamUrl = mock.url;
172
- upstreamToken = mock.authToken;
202
+ upstreamOptions = { remoteUrl: mock.url, authToken: mock.authToken };
173
203
  backend = "mock-upstream";
174
204
  }
175
205
  const spawnProxyClient = async (withAuth) => {
176
206
  const childEnv = {
177
207
  ...process.env,
178
- HONUA_MCP_REMOTE_URL: upstreamUrl,
208
+ HONUA_MCP_REMOTE_URL: upstreamOptions.remoteUrl,
179
209
  };
180
- if (withAuth && upstreamToken) {
181
- childEnv.HONUA_MCP_AUTH_TOKEN = upstreamToken;
210
+ delete childEnv.HONUA_MCP_AUTH_TOKEN;
211
+ delete childEnv.HONUA_ADMIN_KEY;
212
+ delete childEnv.HONUA_API_KEY;
213
+ if (withAuth && upstreamOptions.authToken) {
214
+ childEnv.HONUA_MCP_AUTH_TOKEN = upstreamOptions.authToken;
215
+ }
216
+ else if (withAuth && upstreamOptions.apiKey) {
217
+ childEnv.HONUA_API_KEY = upstreamOptions.apiKey;
182
218
  }
183
219
  else {
184
- delete childEnv.HONUA_MCP_AUTH_TOKEN;
185
- delete childEnv.HONUA_API_KEY;
220
+ // Keep the certification's anonymous negative control credential-free.
186
221
  }
187
222
  const transport = new StdioClientTransport({
188
223
  command: process.execPath,
@@ -199,8 +234,8 @@ async function openStdioProxyTarget(env) {
199
234
  return {
200
235
  mode: "stdio-proxy",
201
236
  backend,
202
- serverLabel: `honua-mcp-proxy → ${backend === "live" ? upstreamUrl : "in-process mock /mcp"}`,
203
- authMode: upstreamToken ? "bearer" : env.HONUA_API_KEY ? "api-key" : "anonymous",
237
+ serverLabel: `honua-mcp-proxy → ${backend === "live" ? upstreamOptions.remoteUrl : "in-process mock /mcp"}`,
238
+ authMode: upstreamOptions.authToken ? "bearer" : upstreamOptions.apiKey ? "api-key" : "anonymous",
204
239
  honuaTransport: env.HONUA_TRANSPORT ?? null,
205
240
  client,
206
241
  supportsUnauthenticatedPass: true,
@@ -0,0 +1,3 @@
1
+ /** Validate an HTTP endpoint before any transport is created. */
2
+ export declare function requireSecureCredentialEndpoint(value: string, label: string, credentialed?: boolean): URL;
3
+ //# sourceMappingURL=credential-endpoint.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"credential-endpoint.d.ts","sourceRoot":"","sources":["../../src/credential-endpoint.ts"],"names":[],"mappings":"AAAA,iEAAiE;AACjE,wBAAgB,+BAA+B,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,YAAY,UAAO,GAAG,GAAG,CAiBtG"}
@@ -0,0 +1,24 @@
1
+ /** Validate an HTTP endpoint before any transport is created. */
2
+ export function requireSecureCredentialEndpoint(value, label, credentialed = true) {
3
+ let url;
4
+ try {
5
+ url = new URL(value);
6
+ }
7
+ catch {
8
+ throw new Error(`${label} must be a valid absolute URL`);
9
+ }
10
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
11
+ throw new Error(`${label} must use http or https`);
12
+ }
13
+ if (url.username || url.password || url.search || url.hash) {
14
+ throw new Error(`${label} must not include embedded credentials, query parameters, or a fragment`);
15
+ }
16
+ if (credentialed && url.protocol === "http:" && !isLoopbackHost(url.hostname)) {
17
+ throw new Error(`${label} requires HTTPS except for exact loopback HTTP development endpoints`);
18
+ }
19
+ return url;
20
+ }
21
+ function isLoopbackHost(hostname) {
22
+ const normalized = hostname.toLowerCase();
23
+ return normalized === "localhost" || normalized === "127.0.0.1" || normalized === "::1" || normalized === "[::1]";
24
+ }
@@ -22,10 +22,12 @@ import { runEval } from "./runner.js";
22
22
  * explicit request yields an honest driverError, not a silent
23
23
  * skip. Without --driver, drivers are auto-resolved from env.
24
24
  * --corpus <name> Select the corpus explicitly: analyst | operator | northstar |
25
- * all. This overrides HONUA_EVAL_CORPUS and is how the `eval:live`
25
+ * standalone | ogc | all. This overrides HONUA_EVAL_CORPUS and is how the `eval:live`
26
26
  * script targets the operator surface without relying on shell env
27
27
  * (cross-platform). `northstar` targets the three P1-gate
28
- * workflows. Defaults to the env-driven selection.
28
+ * workflows; `standalone` and `ogc` target the two platform-free
29
+ * fixture surfaces (a plain Esri FeatureServer and a plain OGC API
30
+ * Features endpoint). Defaults to the env-driven selection.
29
31
  *
30
32
  * Corpus selection is env-driven (see `resolveCorpus`): default is the analyst
31
33
  * corpus (in-process SDK fixture surface, the CI control); `HONUA_EVAL_CORPUS=operator`
@@ -37,7 +39,7 @@ import { runEval } from "./runner.js";
37
39
  * chain), and live remote MCP runs when HONUA_MCP_REMOTE_URL is set. Keys are
38
40
  * never hardcoded — they come from the environment.
39
41
  */
40
- const CORPUS_NAMES = ["analyst", "operator", "northstar", "standalone", "all"];
42
+ const CORPUS_NAMES = ["analyst", "operator", "northstar", "standalone", "ogc", "all"];
41
43
  function parseArgs(argv) {
42
44
  let artifactOnly = false;
43
45
  let offline = false;
@@ -93,6 +95,7 @@ async function main() {
93
95
  ...(drivers.length > 0 ? { drivers: selectDrivers(drivers) } : {}),
94
96
  ...(corpus ? { corpus: corpusForName(corpus) } : {}),
95
97
  ...(corpus === "standalone" ? { forceStandaloneSurface: true } : {}),
98
+ ...(corpus === "ogc" ? { forceOgcSurface: true } : {}),
96
99
  });
97
100
  mkdirSync(outDir, { recursive: true });
98
101
  const jsonPath = resolve(outDir, "mcp-eval-results.json");
@@ -31,4 +31,11 @@ export declare function resolveCorpus(env?: NodeJS.ProcessEnv): Scenario[];
31
31
  * corpus resolves against real recorded non-Honua data.
32
32
  */
33
33
  export declare function isStandaloneCorpus(env?: NodeJS.ProcessEnv): boolean;
34
+ /**
35
+ * Whether a corpus selection targets the NON-GeoServices surface (issue #1005) —
36
+ * a plain OGC API Features endpoint replayed from recorded pygeoapi collections.
37
+ * The offline runner uses this to pick the OGC fixture client, on which every
38
+ * GeoServices entry point rejects.
39
+ */
40
+ export declare function isOgcCorpus(env?: NodeJS.ProcessEnv): boolean;
34
41
  //# sourceMappingURL=corpus.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"corpus.d.ts","sourceRoot":"","sources":["../../../src/eval/corpus.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAE3C;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,MAAM,EAAE,QAAQ,EAkG5B,CAAC;AAEF;;;;;;;;;;GAUG;AACH,wBAAgB,aAAa,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,QAAQ,EAAE,CAiB9E;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,OAAO,CAEhF"}
1
+ {"version":3,"file":"corpus.d.ts","sourceRoot":"","sources":["../../../src/eval/corpus.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAE3C;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,MAAM,EAAE,QAAQ,EAkG5B,CAAC;AAEF;;;;;;;;;;GAUG;AACH,wBAAgB,aAAa,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,QAAQ,EAAE,CAmB9E;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,OAAO,CAEhF;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,OAAO,CAEzE"}
@@ -1,4 +1,5 @@
1
1
  import { NORTHSTAR_CORPUS } from "./northstar-corpus.js";
2
+ import { OGC_CORPUS } from "./ogc-corpus.js";
2
3
  import { OPERATOR_CORPUS } from "./operator-corpus.js";
3
4
  import { STANDALONE_CORPUS } from "./standalone-corpus.js";
4
5
  /**
@@ -129,6 +130,8 @@ export function resolveCorpus(env = process.env) {
129
130
  return NORTHSTAR_CORPUS;
130
131
  case "standalone":
131
132
  return STANDALONE_CORPUS;
133
+ case "ogc":
134
+ return OGC_CORPUS;
132
135
  case "all":
133
136
  // `all` spans the Honua-surface corpora only. The platform-free standalone
134
137
  // corpus targets a DIFFERENT surface (a plain FeatureServer fixture) and is
@@ -148,3 +151,12 @@ export function resolveCorpus(env = process.env) {
148
151
  export function isStandaloneCorpus(env = process.env) {
149
152
  return (env.HONUA_EVAL_CORPUS ?? "").trim().toLowerCase() === "standalone";
150
153
  }
154
+ /**
155
+ * Whether a corpus selection targets the NON-GeoServices surface (issue #1005) —
156
+ * a plain OGC API Features endpoint replayed from recorded pygeoapi collections.
157
+ * The offline runner uses this to pick the OGC fixture client, on which every
158
+ * GeoServices entry point rejects.
159
+ */
160
+ export function isOgcCorpus(env = process.env) {
161
+ return (env.HONUA_EVAL_CORPUS ?? "").trim().toLowerCase() === "ogc";
162
+ }
@@ -0,0 +1,3 @@
1
+ import type { Scenario } from "./types.js";
2
+ export declare const OGC_CORPUS: Scenario[];
3
+ //# sourceMappingURL=ogc-corpus.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ogc-corpus.d.ts","sourceRoot":"","sources":["../../../src/eval/ogc-corpus.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAqC3C,eAAO,MAAM,UAAU,EAAE,QAAQ,EAgahC,CAAC"}