@honua/mcp-server 0.1.12 → 0.1.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -256,9 +256,34 @@ atomic approval-use store can opt into two additional tools:
256
256
  expiry, single-use, cancellation, and plan-identity checks. The envelope is required for effects and for every
257
257
  read step that names a `sourceId`; only non-source inspection may use read-only auto-execution.
258
258
 
259
- ```ts
259
+ Embedding the server in your own Node.js host needs the packages as local
260
+ dependencies of that host project:
261
+
262
+ ```bash
263
+ npm install @honua/mcp-server@0.1.13 @honua/sdk-js@0.1.13
264
+ ```
265
+
266
+ The map runtime, model callback, approval verifier, receipt signer, and
267
+ approval-use store are yours to supply; the `declare const` lines stand in for them:
268
+
269
+ ```ts doc-test=compile
270
+ import { HonuaClient } from "@honua/sdk-js";
271
+ import type {
272
+ AgentApprovalUseConsumer,
273
+ AgentEnvelopeSigner,
274
+ AgentEnvelopeVerifier,
275
+ } from "@honua/sdk-js/agent-safety";
276
+ import type { CreateNlMapControlOptions, NlMapControlToolsOptions } from "@honua/sdk-js/nl-map-control";
260
277
  import { createNlMapControlMcpHost, createServer } from "@honua/mcp-server";
261
278
 
279
+ declare const runtime: NlMapControlToolsOptions["runtime"];
280
+ declare const callYourModel: CreateNlMapControlOptions["llm"];
281
+ declare const approvalVerifier: AgentEnvelopeVerifier;
282
+ declare const receiptSigner: AgentEnvelopeSigner;
283
+ declare const approvalUseConsumer: AgentApprovalUseConsumer;
284
+
285
+ const honuaClient = new HonuaClient({ baseUrl: "https://services.arcgis.com/P3ePLMYs2RVChkJx/arcgis" });
286
+
262
287
  const nlMapControl = createNlMapControlMcpHost({
263
288
  control: {
264
289
  tools: { runtime },
@@ -311,6 +336,26 @@ For each tool the server advertises (over an in-memory MCP transport) it:
311
336
  It emits a stable machine-readable JSON report plus a human-readable Markdown
312
337
  summary, and exits non-zero on any conformance/round-trip failure.
313
338
 
339
+ The certification and eval scripts in this section and the next run from a clone
340
+ of this repository (Node.js `>=20.19` and git). `mcp/` links to the SDK at the
341
+ repository root, so build the SDK first:
342
+
343
+ ```bash
344
+ git clone --depth 1 https://github.com/honua-io/honua-sdk-js.git
345
+ cd honua-sdk-js
346
+ npm ci
347
+ ```
348
+
349
+ <!-- doc-run: checkout -->
350
+ ```bash
351
+ npm run build # the SDK that mcp/ links to; takes a few minutes
352
+ cd mcp
353
+ npm ci
354
+ ```
355
+
356
+ From `mcp/`:
357
+
358
+ <!-- doc-run: checkout=mcp -->
314
359
  ```bash
315
360
  # Run the certifier against the offline fixture backend and write artifacts:
316
361
  npm run certify
@@ -336,9 +381,14 @@ apportionment layer from `services.arcgis.com`, with **no** Honua surfaces. It
336
381
  proves the tools certify **green with honest skips** against a non-Honua endpoint:
337
382
  the data tools round-trip against real recorded data, the Honua-only style tools
338
383
  degrade to structured "not available" results, and the auth/mutation/job contracts
339
- skip-with-reason. Fixture-backed and deterministic — no network.
384
+ skip-with-reason. Fixture-backed and deterministic — no network — as long as
385
+ `HONUA_BASE_URL` is unset: when it is set, this lane certifies that live endpoint
386
+ instead (the scheduled live variant). Unset it if you exported it for `honua-mcp`
387
+ above:
340
388
 
389
+ <!-- doc-run: checkout=mcp -->
341
390
  ```bash
391
+ unset HONUA_BASE_URL
342
392
  npm run certify:standalone # platform-free cert (plain FeatureServer fixture)
343
393
  npm run test:certification:standalone # CI gate variant
344
394
  ```
@@ -359,6 +409,7 @@ in `config/live-conformance-endpoints.v1.json`). Nothing Esri exists there: the
359
409
  GeoServices entry points all reject, so a tool that still secretly required
360
410
  `serviceId`/`layerId` fails loudly instead of passing by accident.
361
411
 
412
+ <!-- doc-run: checkout=mcp -->
362
413
  ```bash
363
414
  npm run certify:standalone-ogc # non-GeoServices cert (OGC API Features fixture)
364
415
  npm run test:certification:standalone-ogc # CI gate variant
@@ -390,8 +441,8 @@ The honua server exposes one MCP catalog over streamable-HTTP/SSE at `/mcp`.
390
441
  Claude-Desktop-style clients speak **stdio**. Rather than reimplement that
391
442
  catalog (which is how the HTTP and stdio surfaces historically drifted apart),
392
443
  this package ships a **stdio proxy** (`honua-mcp-proxy`) that bridges a local
393
- stdio MCP client to the remote HTTP-SSE MCP server. It connects upstream as an
394
- MCP client and re-exposes the *same* catalog downstream over stdio — identical
444
+ stdio MCP client to the remote HTTP-SSE MCP server. It forwards MCP messages
445
+ between the transports and exposes the *same* catalog downstream over stdio — identical
395
446
  tools, identical input/output schemas, identical resources and prompts, and live
396
447
  `tools/list_changed` notifications. There is one source-of-truth catalog (the
397
448
  server's `/mcp`); the SDK proxies it, so the two transports are symmetric by
@@ -417,8 +468,33 @@ or `[::1]` HTTP development endpoints. Userinfo, query parameters, fragments,
417
468
  non-HTTP(S) URLs are rejected before connect, and redirect targets are never
418
469
  followed or sent credentials.
419
470
 
420
- A parity test (`test/proxy.test.ts`) asserts the tool/resource/template catalog
421
- the downstream client sees is byte-identical to the upstream surface, that
471
+ The proxy forwards the caller's `initialize` request before opening an initialized
472
+ upstream session, including `_meta["honua.io/workflow-view"]`. That HTTP session
473
+ is retained for subsequent requests, so initializing with `setup` makes a later
474
+ selector-free `tools/list` return the server's setup catalog. A temporary
475
+ per-request catalog override leaves the initialized view intact; separate proxy
476
+ processes have separate sessions.
477
+
478
+ The view selector is the only `initialize` metadata the server reads, and the
479
+ only metadata the proxy forwards: other `_meta` keys stay local rather than
480
+ entering the credentialed upstream session. The handshake fails closed. A
481
+ request before `initialize`, an `initialize` the server's selector contract
482
+ would refuse (a non-string or longer-than-64-character view), or a second
483
+ `initialize` is answered with JSON-RPC `-32600` without reaching the server,
484
+ and the proxy exits. If `initialize` does not arrive, or the server does not
485
+ answer it, within 30 seconds, the proxy exits as well.
486
+
487
+ Executable protocol tests (`test/proxy-session.test.ts`) exercise setup/default
488
+ initialization, concurrent sessions, authenticated full-catalog override and
489
+ restoration, `tools/list_changed` relay, and the fail-closed handshake against a
490
+ loopback fixture. The `MCP session contract` workflow also runs them against the
491
+ `honua-mcp-proxy` executable npm installs from the packed tarballs. The
492
+ published 0.1.12 reproduction and exact JSON wire captures are under
493
+ `test/fixtures/initialize-view/`.
494
+
495
+ A parity test (`test/proxy.test.ts`) runs the same bridge against the canonical
496
+ server over streamable HTTP. It asserts the tool/resource/template catalog the
497
+ downstream client sees is byte-identical to a direct HTTP client's, that
422
498
  `tools/call` and resource reads round-trip identically, and that `list_changed`
423
499
  notifications are forwarded.
424
500
 
@@ -449,6 +525,7 @@ honua-io/honua-server#1956.)
449
525
  dependencies); keys come from the environment and are never hardcoded. Set
450
526
  `HONUA_MCP_REMOTE_URL` to drive a live remote `/mcp` instead of the fixture.
451
527
 
528
+ <!-- doc-run: checkout=mcp -->
452
529
  ```bash
453
530
  npm run eval # run the eval (live models join if their keys are set)
454
531
  npm run eval:offline # force the deterministic control + fixture surface
@@ -474,6 +551,7 @@ guards. Because the fixture replays real recorded data, a wrong number or a
474
551
  hallucinated place name fails. The grading taxonomy is documented in
475
552
  [`evals/README.md`](evals/README.md).
476
553
 
554
+ <!-- doc-run: checkout=mcp -->
477
555
  ```bash
478
556
  npm run eval:standalone # deterministic control over the census fixture (offline)
479
557
  ```
@@ -492,6 +570,7 @@ an extent that came from the declared collection extent must say so, and a CQL2
492
570
  spatial predicate the endpoint does not publish must come back as a structured
493
571
  capability refusal — never as an empty result set.
494
572
 
573
+ <!-- doc-run: checkout=mcp -->
495
574
  ```bash
496
575
  npm run eval:ogc # deterministic control over the OGC API Features fixture (offline)
497
576
  ```
@@ -527,6 +606,12 @@ Shared env for every live run:
527
606
  Set exactly one of `HONUA_MCP_AUTH_TOKEN` or `HONUA_API_KEY` for authenticated
528
607
  live runs; ambiguous authentication fails closed.
529
608
 
609
+ The commands below read three values you supply from your own accounts: `HONUA_TOKEN`
610
+ (a bearer token your operator deployment issued), and the model provider's
611
+ `ANTHROPIC_API_KEY` or `OPENAI_API_KEY` (the Bedrock driver uses the AWS credential
612
+ chain instead). Export the ones your driver needs before you run it, and run from `mcp/`:
613
+
614
+ <!-- doc-run: skip reason="billable live-model lane: needs the reader's own operator token and Anthropic, OpenAI or AWS model credentials; the offline control above (npm run eval:offline) is the runnable lane" -->
530
615
  ```bash
531
616
  # Anthropic Claude (default claude-opus-4-8; override with HONUA_EVAL_ANTHROPIC_MODEL):
532
617
  HONUA_MCP_REMOTE_URL="https://demo.honua.io/mcp" \
@@ -4,7 +4,7 @@ import { ADMIN_MCP_CONTRACT_SERVER_SHA, ADMIN_MCP_COVERAGE_SHA256, ADMIN_MCP_EXC
4
4
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
5
5
  import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
6
6
  import { isMainEntrypoint } from "../entrypoint.js";
7
- import { connectUpstream, createProxyServer, resolveProxyOptions } from "../proxy.js";
7
+ import { connectProxyTransport, connectUpstream, resolveProxyOptions } from "../proxy.js";
8
8
  const DEFAULT_EXPECTATION = {
9
9
  publishedToolNames: ADMIN_MCP_PUBLISHED_TOOL_NAMES,
10
10
  excludedToolNames: ADMIN_MCP_EXCLUDED_OPERATIONS.map((operation) => operation.toolName),
@@ -202,12 +202,13 @@ export async function runAdminParityCertification(env = process.env, outputPath
202
202
  if (ADMIN_MCP_CONTRACT_SERVER_SHA === null) {
203
203
  throw new Error("Admin MCP live certification is blocked until config/admin-client.v1.json pins the final server head.");
204
204
  }
205
- const upstream = await connectUpstream(resolveProxyOptions(env));
206
- const proxy = createProxyServer(upstream);
205
+ const options = resolveProxyOptions(env);
206
+ const upstream = await connectUpstream(options);
207
+ // The proxied side runs through the same bridge the stdio executable uses.
207
208
  const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
208
209
  const proxied = new Client({ name: "honua-admin-parity", version: "1.0.0" });
209
210
  try {
210
- await proxy.connect(serverTransport);
211
+ await connectProxyTransport(options, serverTransport);
211
212
  await proxied.connect(clientTransport);
212
213
  const directTools = await listAllTools(upstream);
213
214
  const proxiedTools = await listAllTools(proxied);
@@ -219,7 +220,6 @@ export async function runAdminParityCertification(env = process.env, outputPath
219
220
  }
220
221
  finally {
221
222
  await proxied.close().catch(() => { });
222
- await proxy.close().catch(() => { });
223
223
  await upstream.close().catch(() => { });
224
224
  }
225
225
  }
package/dist/src/index.js CHANGED
@@ -172,10 +172,10 @@ export function createServer(client, options = {}) {
172
172
  registerNlMapControlMcpTools(server, options.nlMapControl);
173
173
  }
174
174
  server.tool("honua_describe_layer", "Describe a source: its protocol, the capabilities it actually supports, field schema, geometry type, and extent. Address it with `source` (from honua_list_sources); the serviceId/layerId pair is deprecated but still accepted.", describeLayer.schema.shape, async (args) => describeLayer.execute(client, describeLayer.schema.parse(args)));
175
- server.tool("honua_query_features", "Query features from any supported protocol. Address the source with `source`; filter with the typed protocol-neutral `filter`, a GeoJSON `geometry` or `bbox` plus `spatialRel`, and `temporal`. returnGeometry defaults to false to save tokens. A construct the backing protocol cannot express returns a structured capability error, never a silently empty result.", queryFeatures.schema.shape, async (args) => queryFeatures.execute(client, queryFeatures.schema.parse(args)));
176
- server.tool("honua_count_features", "Count features matching a filter without returning data. Use before querying to check cardinality.", countFeatures.schema.shape, async (args) => countFeatures.execute(client, countFeatures.schema.parse(args)));
177
- server.tool("honua_get_extent", "Get the spatial bounding box of features matching a filter.", getExtent.schema.shape, async (args) => getExtent.execute(client, getExtent.schema.parse(args)));
178
- server.tool("honua_statistics", "Compute aggregate statistics (count, sum, avg, min, max, stddev, var) on a field, optionally grouped. Protocols without server-side aggregation answer client-side and report the degradation.", statistics.schema.shape, async (args) => statistics.execute(client, statistics.schema.parse(args)));
175
+ server.tool("honua_query_features", "Query features from any supported protocol. Address the source with `source`; filter with the typed protocol-neutral `filter`, a GeoJSON `geometry` or `bbox` plus `spatialRel`, and `temporal`. returnGeometry defaults to false to save tokens. A construct the backing protocol cannot express returns a structured capability error, never a silently empty result.", queryFeatures.schema.shape, async (args, extra) => queryFeatures.execute(client, queryFeatures.schema.parse(args), extra?.signal));
176
+ server.tool("honua_count_features", "Count features matching a filter without returning data. Use before querying to check cardinality.", countFeatures.schema.shape, async (args, extra) => countFeatures.execute(client, countFeatures.schema.parse(args), extra?.signal));
177
+ server.tool("honua_get_extent", "Get the spatial bounding box of features matching a filter.", getExtent.schema.shape, async (args, extra) => getExtent.execute(client, getExtent.schema.parse(args), extra?.signal));
178
+ server.tool("honua_statistics", "Compute aggregate statistics (count, sum, avg, min, max, stddev, var) on a field, optionally grouped. Protocols without server-side aggregation answer client-side and report the degradation.", statistics.schema.shape, async (args, extra) => statistics.execute(client, statistics.schema.parse(args), extra?.signal));
179
179
  server.tool("honua_explain_capability_gap", "Explain whether a source/protocol supports a capability and suggest a safe fallback when it does not.", explainCapabilityGap.schema.shape, async (args) => explainCapabilityGap.execute(client, explainCapabilityGap.schema.parse(args)));
180
180
  server.tool("honua_get_style", "Resolve a style from a server's OGC API – Styles surface. Pass styleId for the canonical StyleRef (style_id, title, encodings incl. inlined MapLibre); omit it to list available styles. On a plain FeatureServer with no styling surface, reports a structured 'not available on this target' result.", getStyle.schema.shape, async (args) => getStyle.execute(client, getStyle.schema.parse(args)));
181
181
  server.tool("honua_apply_style_preset", "Resolve a style preset and its MapLibre stylesheet for client-side application. Read-only: returns the stylesheet to apply locally; it does not mutate server state. Degrades gracefully on a target with no styling surface.", applyStylePreset.schema.shape, async (args) => applyStylePreset.execute(client, applyStylePreset.schema.parse(args)));
@@ -16,9 +16,9 @@ export type GeometryFormat = "geojson" | "esri-json";
16
16
  export declare function projectGeometry(geometry: Record<string, unknown> | null | undefined, format: GeometryFormat): Record<string, unknown> | null;
17
17
  /** Normalize `Result.degraded` for tool output; `undefined` when nothing degraded. */
18
18
  export declare function projectDegraded(degraded: readonly DegradedReason[] | undefined): {
19
- capability: import("@honua/sdk-js/contract").Capability;
19
+ capability: import("@honua/sdk-js").Capability;
20
20
  reason: string;
21
- protocol: import("@honua/sdk-js/contract").Protocol | null;
21
+ protocol: import("@honua/sdk-js").Protocol | null;
22
22
  sourceId: string | null;
23
23
  }[] | undefined;
24
24
  export interface ProjectFeaturesOptions {
@@ -1 +1 @@
1
- {"version":3,"file":"result.d.ts","sourceRoot":"","sources":["../../../src/neutral/result.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,EAAE,MAAM,wBAAwB,CAAC;AAGrE;;;;;;;;;;;GAWG;AAEH,MAAM,MAAM,cAAc,GAAG,SAAS,GAAG,WAAW,CAAC;AAMrD,gEAAgE;AAChE,wBAAgB,eAAe,CAC7B,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,GAAG,SAAS,EACpD,MAAM,EAAE,cAAc,GACrB,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAMhC;AAED,sFAAsF;AACtF,wBAAgB,eAAe,CAAC,QAAQ,EAAE,SAAS,cAAc,EAAE,GAAG,SAAS;;;;;gBAQ9E;AAED,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,cAAc,EAAE,OAAO,CAAC;IACjC,QAAQ,CAAC,cAAc,EAAE,cAAc,CAAC;CACzC;AAED,wEAAwE;AACxE,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,sBAAsB;;;IAU9E"}
1
+ {"version":3,"file":"result.d.ts","sourceRoot":"","sources":["../../../src/neutral/result.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,EAAE,MAAM,wBAAwB,CAAC;AAGrE;;;;;;;;;;;GAWG;AAEH,MAAM,MAAM,cAAc,GAAG,SAAS,GAAG,WAAW,CAAC;AAMrD,gEAAgE;AAChE,wBAAgB,eAAe,CAC7B,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,GAAG,SAAS,EACpD,MAAM,EAAE,cAAc,GACrB,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAWhC;AAED,sFAAsF;AACtF,wBAAgB,eAAe,CAAC,QAAQ,EAAE,SAAS,cAAc,EAAE,GAAG,SAAS;;;;;gBAQ9E;AAED,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,cAAc,EAAE,OAAO,CAAC;IACjC,QAAQ,CAAC,cAAc,EAAE,cAAc,CAAC;CACzC;AAED,wEAAwE;AACxE,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,sBAAsB;;;IAU9E"}
@@ -1,13 +1,20 @@
1
- import { esriToGeoJson } from "@honua/sdk-js/geometry";
1
+ import { HonuaCapabilityNotSupportedError } from "@honua/sdk-js";
2
+ import { esriToGeoJson, geoJsonToEsri } from "@honua/sdk-js/geometry";
2
3
  function looksGeoJson(geometry) {
3
- return typeof geometry.type === "string" && "coordinates" in geometry;
4
+ return typeof geometry.type === "string" && ("coordinates" in geometry || geometry.type === "GeometryCollection");
4
5
  }
5
6
  /** Re-encode one feature geometry into the requested format. */
6
7
  export function projectGeometry(geometry, format) {
7
8
  if (!geometry)
8
9
  return null;
9
- if (format === "esri-json")
10
- return geometry;
10
+ if (format === "esri-json") {
11
+ if (!looksGeoJson(geometry))
12
+ return geometry;
13
+ const converted = geoJsonToEsri(geometry);
14
+ if (!converted)
15
+ throw new HonuaCapabilityNotSupportedError("geometryFormat.esri-json", "geometry-projection");
16
+ return converted;
17
+ }
11
18
  if (looksGeoJson(geometry))
12
19
  return geometry;
13
20
  const converted = esriToGeoJson(geometry);
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
3
- import { Server } from "@modelcontextprotocol/sdk/server/index.js";
3
+ import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
4
+ import { type InitializeRequest } from "@modelcontextprotocol/sdk/types.js";
4
5
  /**
5
6
  * Transport-symmetric stdio proxy for the honua MCP surface (honua-server #1950).
6
7
  *
@@ -8,10 +9,12 @@ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
8
9
  * `/mcp`. Claude-Desktop-style clients speak stdio. Rather than reimplementing
9
10
  * the tool/resource catalog (the older `@honua/mcp-server` discovery surface did
10
11
  * exactly that, which is how the two halves drifted apart), this proxy bridges a
11
- * local stdio MCP client to the remote HTTP-SSE MCP server: it connects upstream
12
- * as an MCP client, then re-exposes the *same* catalog downstream over stdio.
12
+ * local stdio MCP client to the remote HTTP-SSE MCP server: each stdio client
13
+ * gets one upstream streamable-HTTP session, opened by the client's own
14
+ * initialize, and every later message is relayed over that session.
13
15
  *
14
- * Because every request and notification is forwarded verbatim, the stdio
16
+ * Because every request and notification is forwarded verbatim (initialize's
17
+ * `_meta` is narrowed to the workflow-view selector), the stdio
15
18
  * surface is transport-symmetric with the HTTP-SSE surface by construction —
16
19
  * identical tools, identical input/output schemas, identical resources and
17
20
  * prompts, and live `list_changed` notifications. There is one source-of-truth
@@ -28,19 +31,32 @@ export interface ProxyOptions {
28
31
  export declare function resolveProxyOptions(env?: NodeJS.ProcessEnv): ProxyOptions;
29
32
  /** Build request headers for the upstream connection from the resolved options. */
30
33
  export declare function buildUpstreamHeaders(options: ProxyOptions): Record<string, string>;
31
- /** Connect an upstream MCP client to the remote honua /mcp over streamable HTTP. */
32
- export declare function connectUpstream(options: ProxyOptions): Promise<Client>;
33
34
  /**
34
- * Build a low-level MCP server that forwards every request and `list_changed`
35
- * notification to the already-connected upstream client. The proxy advertises
36
- * exactly the upstream's name, version, and capabilities, so the downstream
37
- * (stdio) surface mirrors the upstream (HTTP-SSE) surface.
35
+ * Connect a direct MCP client to the remote honua /mcp over streamable HTTP.
36
+ * Certification and evals use it as the HTTP reference side; the stdio proxy
37
+ * does not, because Client.connect would send its own initialize.
38
38
  */
39
- export declare function createProxyServer(upstream: Client): Server;
39
+ export declare function connectUpstream(options: ProxyOptions): Promise<Client>;
40
+ /** The server's only initialize metadata key; the session view selector. */
41
+ export declare const WORKFLOW_VIEW_META_KEY = "honua.io/workflow-view";
42
+ /** Read the optional view selector under the server's own contract. */
43
+ export declare function readWorkflowView(request: InitializeRequest): string | undefined;
44
+ export interface ProxyBridgeOptions {
45
+ /** Bound on initialize arriving and its upstream response; defaults to 30 s. */
46
+ initializeTimeoutMs?: number;
47
+ }
40
48
  /**
41
- * Run the stdio proxy end-to-end: resolve the remote `/mcp` from the
42
- * environment, connect upstream, and expose the mirrored catalog over stdio.
43
- * Also used by the deprecated `honua-mcp` bin, which now delegates here.
49
+ * Bridge one downstream transport to one upstream HTTP session. In particular,
50
+ * forward the caller's initialize instead of letting Client.connect synthesize
51
+ * an earlier initialize without its metadata. The server owns view selection
52
+ * and request overrides; the proxy never supplies a tools/list selector.
53
+ *
54
+ * The handshake fails closed: traffic before initialize, an initialize the
55
+ * server's selector contract would refuse, a second initialize, or a handshake
56
+ * that does not complete within the bound gets a JSON-RPC error (when it has
57
+ * an id) and closes both transports. Nothing invalid reaches the server.
44
58
  */
59
+ export declare function connectProxyTransport(options: ProxyOptions, downstream: Transport, bridge?: ProxyBridgeOptions): Promise<Transport>;
60
+ /** Run the published stdio executable with the caller's initialize intact. */
45
61
  export declare function runProxy(env?: NodeJS.ProcessEnv): Promise<void>;
46
62
  //# sourceMappingURL=proxy.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"proxy.d.ts","sourceRoot":"","sources":["../../src/proxy.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAC;AAEnE,OAAO,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAC;AA0BnE;;;;;;;;;;;;;;;GAeG;AAEH,MAAM,WAAW,YAAY;IAC3B,sFAAsF;IACtF,SAAS,EAAE,MAAM,CAAC;IAClB,wDAAwD;IACxD,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC/B,4EAA4E;IAC5E,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAC7B;AAsBD,wBAAgB,mBAAmB,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,YAAY,CAqBtF;AAED,mFAAmF;AACnF,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,YAAY,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAUlF;AAED,oFAAoF;AACpF,wBAAsB,eAAe,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,MAAM,CAAC,CAS5E;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CA+C1D;AAOD;;;;GAIG;AACH,wBAAsB,QAAQ,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,OAAO,CAAC,IAAI,CAAC,CAgBlF"}
1
+ {"version":3,"file":"proxy.d.ts","sourceRoot":"","sources":["../../src/proxy.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAC;AAGnE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,+CAA+C,CAAC;AAC/E,OAAO,EAEL,KAAK,iBAAiB,EAQvB,MAAM,oCAAoC,CAAC;AAK5C;;;;;;;;;;;;;;;;;GAiBG;AAEH,MAAM,WAAW,YAAY;IAC3B,sFAAsF;IACtF,SAAS,EAAE,MAAM,CAAC;IAClB,wDAAwD;IACxD,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC/B,4EAA4E;IAC5E,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAC7B;AAsBD,wBAAgB,mBAAmB,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,YAAY,CAqBtF;AAED,mFAAmF;AACnF,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,YAAY,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAUlF;AASD;;;;GAIG;AACH,wBAAsB,eAAe,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,MAAM,CAAC,CAI5E;AAED,4EAA4E;AAC5E,eAAO,MAAM,sBAAsB,2BAA2B,CAAC;AAI/D,uEAAuE;AACvE,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,iBAAiB,GAAG,MAAM,GAAG,SAAS,CAO/E;AAgBD,MAAM,WAAW,kBAAkB;IACjC,gFAAgF;IAChF,mBAAmB,CAAC,EAAE,MAAM,CAAC;CAC9B;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,qBAAqB,CACzC,OAAO,EAAE,YAAY,EACrB,UAAU,EAAE,SAAS,EACrB,MAAM,GAAE,kBAAuB,GAC9B,OAAO,CAAC,SAAS,CAAC,CA+FpB;AAED,8EAA8E;AAC9E,wBAAsB,QAAQ,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,OAAO,CAAC,IAAI,CAAC,CAelF"}
package/dist/src/proxy.js CHANGED
@@ -1,18 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
3
3
  import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
4
- import { Server } from "@modelcontextprotocol/sdk/server/index.js";
5
4
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6
- import { CallToolRequestSchema, GetPromptRequestSchema, ListPromptsRequestSchema, ListResourceTemplatesRequestSchema, ListResourcesRequestSchema, ListToolsRequestSchema, PromptListChangedNotificationSchema, ReadResourceRequestSchema, ResourceListChangedNotificationSchema, ToolListChangedNotificationSchema, } from "@modelcontextprotocol/sdk/types.js";
5
+ import { ErrorCode, InitializeRequestSchema, isInitializedNotification, isJSONRPCErrorResponse, isJSONRPCRequest, isJSONRPCResultResponse, } from "@modelcontextprotocol/sdk/types.js";
7
6
  import { requireSecureCredentialEndpoint } from "./credential-endpoint.js";
8
7
  import { isMainEntrypoint } from "./entrypoint.js";
9
8
  import { SERVER_VERSION } from "./index.js";
10
- // MCP permits server extensions such as Honua's tools/list `view`. The SDK's
11
- // nested params schema strips unknown keys by default, so preserve them before
12
- // forwarding or a stdio caller silently receives the default catalog instead.
13
- const ForwardedListToolsRequestSchema = ListToolsRequestSchema.extend({
14
- params: ListToolsRequestSchema.shape.params.unwrap().passthrough().optional(),
15
- });
16
9
  function isConfigured(value) {
17
10
  return value !== undefined && value.length > 0;
18
11
  }
@@ -58,84 +51,172 @@ export function buildUpstreamHeaders(options) {
58
51
  }
59
52
  return headers;
60
53
  }
61
- /** Connect an upstream MCP client to the remote honua /mcp over streamable HTTP. */
62
- export async function connectUpstream(options) {
54
+ function createUpstreamTransport(options) {
63
55
  const headers = buildUpstreamHeaders(options);
64
- const remoteUrl = validateProxyOptions(options);
65
- const transport = new StreamableHTTPClientTransport(remoteUrl, {
56
+ return new StreamableHTTPClientTransport(validateProxyOptions(options), {
66
57
  requestInit: { ...(Object.keys(headers).length > 0 ? { headers } : {}), redirect: "manual" },
67
58
  });
59
+ }
60
+ /**
61
+ * Connect a direct MCP client to the remote honua /mcp over streamable HTTP.
62
+ * Certification and evals use it as the HTTP reference side; the stdio proxy
63
+ * does not, because Client.connect would send its own initialize.
64
+ */
65
+ export async function connectUpstream(options) {
68
66
  const client = new Client({ name: "honua-mcp-stdio-proxy", version: SERVER_VERSION });
69
- await client.connect(transport);
67
+ await client.connect(createUpstreamTransport(options));
70
68
  return client;
71
69
  }
70
+ /** The server's only initialize metadata key; the session view selector. */
71
+ export const WORKFLOW_VIEW_META_KEY = "honua.io/workflow-view";
72
+ const MAX_WORKFLOW_VIEW_LENGTH = 64;
73
+ const INITIALIZE_TIMEOUT_MS = 30_000;
74
+ /** Read the optional view selector under the server's own contract. */
75
+ export function readWorkflowView(request) {
76
+ const value = request.params._meta?.[WORKFLOW_VIEW_META_KEY];
77
+ if (value === undefined || value === null)
78
+ return undefined;
79
+ if (typeof value !== "string" || value.length > MAX_WORKFLOW_VIEW_LENGTH) {
80
+ throw new Error("Invalid workflow view selector");
81
+ }
82
+ return value.trim().length === 0 ? undefined : value;
83
+ }
72
84
  /**
73
- * Build a low-level MCP server that forwards every request and `list_changed`
74
- * notification to the already-connected upstream client. The proxy advertises
75
- * exactly the upstream's name, version, and capabilities, so the downstream
76
- * (stdio) surface mirrors the upstream (HTTP-SSE) surface.
85
+ * Validate the caller's initialize and narrow its `_meta` to the recognised
86
+ * view selector. Every other field is the caller's own and passes unchanged;
87
+ * other metadata stays local rather than entering the credentialed session.
77
88
  */
78
- export function createProxyServer(upstream) {
79
- const upstreamInfo = upstream.getServerVersion() ?? { name: "honua", version: SERVER_VERSION };
80
- const upstreamCapabilities = upstream.getServerCapabilities() ?? {};
81
- const server = new Server({ name: upstreamInfo.name, version: upstreamInfo.version }, {
82
- capabilities: upstreamCapabilities,
83
- instructions: upstream.getInstructions(),
84
- });
85
- // ── Tools ──────────────────────────────────────────────────────
86
- if (upstreamCapabilities.tools) {
87
- server.setRequestHandler(ForwardedListToolsRequestSchema, async (request) => upstream.listTools(request.params));
88
- server.setRequestHandler(CallToolRequestSchema, async (request) => upstream.callTool(request.params));
89
- }
90
- // ── Resources ──────────────────────────────────────────────────
91
- if (upstreamCapabilities.resources) {
92
- server.setRequestHandler(ListResourcesRequestSchema, async (request) => upstream.listResources(request.params));
93
- server.setRequestHandler(ListResourceTemplatesRequestSchema, async (request) => upstream.listResourceTemplates(request.params));
94
- server.setRequestHandler(ReadResourceRequestSchema, async (request) => upstream.readResource(request.params));
95
- }
96
- // ── Prompts ────────────────────────────────────────────────────
97
- if (upstreamCapabilities.prompts) {
98
- server.setRequestHandler(ListPromptsRequestSchema, async (request) => upstream.listPrompts(request.params));
99
- server.setRequestHandler(GetPromptRequestSchema, async (request) => upstream.getPrompt(request.params));
100
- }
101
- // ── list_changed notification forwarding ───────────────────────
102
- // Keeps the stdio surface live: when the server's catalog changes, the
103
- // downstream client is told, exactly as an HTTP-SSE client would be.
104
- upstream.setNotificationHandler(ToolListChangedNotificationSchema, async () => {
105
- await server.sendToolListChanged();
106
- });
107
- upstream.setNotificationHandler(ResourceListChangedNotificationSchema, async () => {
108
- await server.sendResourceListChanged();
109
- });
110
- upstream.setNotificationHandler(PromptListChangedNotificationSchema, async () => {
111
- await server.sendPromptListChanged();
112
- });
113
- return server;
89
+ function upstreamInitialize(message) {
90
+ const view = readWorkflowView(InitializeRequestSchema.parse(message));
91
+ const { _meta: _ignored, ...params } = message.params ?? {};
92
+ return {
93
+ ...message,
94
+ params: { ...params, ...(view === undefined ? {} : { _meta: { [WORKFLOW_VIEW_META_KEY]: view } }) },
95
+ };
114
96
  }
115
- /* v8 ignore start -- live-process entry: wires the stdio transport to a real
116
- remote /mcp upstream; exercised by running the proxy, not by unit tests. The
117
- unit-testable logic (option/header resolution, upstream connect, catalog
118
- forwarding) lives in the exported functions above and is covered there. */
119
97
  /**
120
- * Run the stdio proxy end-to-end: resolve the remote `/mcp` from the
121
- * environment, connect upstream, and expose the mirrored catalog over stdio.
122
- * Also used by the deprecated `honua-mcp` bin, which now delegates here.
98
+ * Bridge one downstream transport to one upstream HTTP session. In particular,
99
+ * forward the caller's initialize instead of letting Client.connect synthesize
100
+ * an earlier initialize without its metadata. The server owns view selection
101
+ * and request overrides; the proxy never supplies a tools/list selector.
102
+ *
103
+ * The handshake fails closed: traffic before initialize, an initialize the
104
+ * server's selector contract would refuse, a second initialize, or a handshake
105
+ * that does not complete within the bound gets a JSON-RPC error (when it has
106
+ * an id) and closes both transports. Nothing invalid reaches the server.
123
107
  */
124
- export async function runProxy(env = process.env) {
125
- const options = resolveProxyOptions(env);
126
- const upstream = await connectUpstream(options);
127
- const server = createProxyServer(upstream);
128
- // Tear down both ends together so a dropped upstream surfaces to the client.
108
+ export async function connectProxyTransport(options, downstream, bridge = {}) {
109
+ const upstream = createUpstreamTransport(options);
110
+ let closed = false;
111
+ let refused = false;
112
+ let initializeSeen = false;
113
+ let initializeId;
114
+ let initialized = Promise.resolve();
129
115
  const shutdown = async () => {
130
- await server.close().catch(() => { });
131
- await upstream.close().catch(() => { });
116
+ if (closed)
117
+ return;
118
+ closed = true;
119
+ clearTimeout(handshake);
120
+ await Promise.all([downstream.close().catch(() => { }), upstream.close().catch(() => { })]);
121
+ };
122
+ // Reply before closing so the caller learns why its session ended. The
123
+ // message is the proxy's own; nothing from upstream is echoed.
124
+ const refuse = (id, code, reason) => {
125
+ if (closed || refused)
126
+ return;
127
+ refused = true;
128
+ const reply = id === undefined ? Promise.resolve() : downstream.send({ jsonrpc: "2.0", id, error: { code, message: reason } });
129
+ void reply.catch(() => { }).finally(() => shutdown());
130
+ };
131
+ const handshake = setTimeout(() => refuse(initializeId, ErrorCode.RequestTimeout, "Proxy initialization timed out"), bridge.initializeTimeoutMs ?? INITIALIZE_TIMEOUT_MS);
132
+ downstream.onclose = upstream.onclose = () => {
133
+ void shutdown();
132
134
  };
133
- upstream.onclose = () => {
135
+ // HTTP/SSE errors can contain credential-bearing response bodies. Close
136
+ // both transports on synchronous or background failures without echoing
137
+ // those details. In particular, a failed SSE reader must not strand stdio.
138
+ upstream.onerror = () => {
134
139
  void shutdown();
135
140
  };
141
+ downstream.onmessage = (message) => {
142
+ if (closed || refused)
143
+ return;
144
+ let forwarded = message;
145
+ const isInitialize = isJSONRPCRequest(message) && message.method === "initialize";
146
+ if (isJSONRPCRequest(message) && message.method === "initialize") {
147
+ if (initializeSeen) {
148
+ refuse(message.id, ErrorCode.InvalidRequest, "Duplicate initialize is not permitted");
149
+ return;
150
+ }
151
+ try {
152
+ forwarded = upstreamInitialize(message);
153
+ }
154
+ catch {
155
+ refuse(message.id, ErrorCode.InvalidRequest, "Invalid initialize request or workflow view selector");
156
+ return;
157
+ }
158
+ initializeSeen = true;
159
+ initializeId = message.id;
160
+ }
161
+ else if (!initializeSeen) {
162
+ refuse(isJSONRPCRequest(message) ? message.id : undefined, ErrorCode.InvalidRequest, "Initialize must be the first request");
163
+ return;
164
+ }
165
+ const sending = initialized.then(() => upstream.send(forwarded));
166
+ // HTTP POSTs may otherwise overtake initialize (which establishes the
167
+ // session) or the initialized notification. Await their acceptance before
168
+ // subsequent traffic, while allowing later tool calls and cancellation
169
+ // notifications to proceed concurrently.
170
+ if (isInitialize || isInitializedNotification(message))
171
+ initialized = sending;
172
+ void sending.catch(() => {
173
+ void shutdown();
174
+ });
175
+ };
176
+ upstream.onmessage = (message) => {
177
+ if (initializeId !== undefined &&
178
+ (isJSONRPCResultResponse(message) || isJSONRPCErrorResponse(message)) &&
179
+ message.id === initializeId) {
180
+ clearTimeout(handshake);
181
+ initializeId = undefined;
182
+ if (isJSONRPCResultResponse(message) && typeof message.result.protocolVersion === "string") {
183
+ upstream.setProtocolVersion(message.result.protocolVersion);
184
+ downstream.setProtocolVersion?.(message.result.protocolVersion);
185
+ }
186
+ }
187
+ void downstream.send(message).catch(() => {
188
+ void shutdown();
189
+ });
190
+ };
191
+ try {
192
+ await upstream.start();
193
+ await downstream.start();
194
+ }
195
+ catch (error) {
196
+ await shutdown();
197
+ throw error;
198
+ }
199
+ return upstream;
200
+ }
201
+ /** Run the published stdio executable with the caller's initialize intact. */
202
+ export async function runProxy(env = process.env) {
203
+ const options = resolveProxyOptions(env);
136
204
  const transport = new StdioServerTransport();
137
- await server.connect(transport);
205
+ const close = () => {
206
+ void transport.close().catch(() => { });
207
+ };
208
+ process.stdin.once("end", close);
209
+ process.stdin.once("close", close);
210
+ try {
211
+ await connectProxyTransport(options, transport);
212
+ }
213
+ catch (error) {
214
+ process.stdin.off("end", close);
215
+ process.stdin.off("close", close);
216
+ throw error;
217
+ }
138
218
  }
219
+ /* v8 ignore start -- process entry; the transport bridge is tested above. */
139
220
  if (isMainEntrypoint(import.meta.url)) {
140
221
  runProxy().catch((err) => {
141
222
  process.stderr.write(`Fatal: ${err instanceof Error ? err.message : String(err)}\n`);
@@ -89,7 +89,7 @@ interface CountOutcome {
89
89
  }
90
90
  /** Resolve a feature count through the canonical `Source` surface. */
91
91
  export declare function countThroughSource(source: CapabilityAwareSource, query: Query, protocol: string, sourceId: string): Promise<CountOutcome>;
92
- export declare function execute(client: HonuaClient, input: Input): Promise<{
92
+ export declare function execute(client: HonuaClient, input: Input, signal?: AbortSignal): Promise<{
93
93
  content: Array<{
94
94
  type: "text";
95
95
  text: string;
@@ -1 +1 @@
1
- {"version":3,"file":"count-features.d.ts","sourceRoot":"","sources":["../../../src/tools/count-features.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAEjD,OAAO,KAAK,EAAE,qBAAqB,EAAE,KAAK,EAAE,MAAM,wBAAwB,CAAC;AAC3E,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAIxB,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAGvD;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAGjB,CAAC;AAEH,MAAM,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,MAAM,CAAC,CAAC;AAE3C,UAAU,YAAY;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,aAAa,GAAG,YAAY,CAAC;IACvC,QAAQ,EAAE,UAAU,CAAC,OAAO,eAAe,CAAC,CAAC;CAC9C;AAED,sEAAsE;AACtE,wBAAsB,kBAAkB,CACtC,MAAM,EAAE,qBAAqB,EAC7B,KAAK,EAAE,KAAK,EACZ,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,YAAY,CAAC,CAevB;AAED,wBAAsB,OAAO,CAAC,MAAM,EAAE,WAAW,EAAE,KAAK,EAAE,KAAK;;;;;;;;;;;;GAmB9D"}
1
+ {"version":3,"file":"count-features.d.ts","sourceRoot":"","sources":["../../../src/tools/count-features.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAEjD,OAAO,KAAK,EAAE,qBAAqB,EAAE,KAAK,EAAE,MAAM,wBAAwB,CAAC;AAC3E,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAIxB,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAGvD;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAGjB,CAAC;AAEH,MAAM,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,MAAM,CAAC,CAAC;AAE3C,UAAU,YAAY;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,aAAa,GAAG,YAAY,CAAC;IACvC,QAAQ,EAAE,UAAU,CAAC,OAAO,eAAe,CAAC,CAAC;CAC9C;AAED,sEAAsE;AACtE,wBAAsB,kBAAkB,CACtC,MAAM,EAAE,qBAAqB,EAC7B,KAAK,EAAE,KAAK,EACZ,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,YAAY,CAAC,CAevB;AAED,wBAAsB,OAAO,CAAC,MAAM,EAAE,WAAW,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,CAAC,EAAE,WAAW;;;;;;;;;;;;GAoBpF"}
@@ -38,10 +38,11 @@ export async function countThroughSource(source, query, protocol, sourceId) {
38
38
  }
39
39
  throw new HonuaCapabilityNotSupportedError("count", protocol, sourceId);
40
40
  }
41
- export async function execute(client, input) {
41
+ export async function execute(client, input, signal) {
42
42
  return withCapabilityHonesty(async () => {
43
43
  const resolved = resolveSource(client, input);
44
44
  const query = toQuery(input, { protocol: resolved.descriptor.protocol, paginate: false });
45
+ query.signal = signal;
45
46
  const outcome = await countThroughSource(resolved.source, query, resolved.descriptor.protocol, resolved.descriptor.id);
46
47
  return jsonText({
47
48
  source: resolved.ref.ref,
@@ -74,7 +74,7 @@ export declare const schema: z.ZodObject<{
74
74
  spatialRel?: "intersects" | "contains" | "within" | "crosses" | "touches" | "overlaps" | "bbox-intersects" | undefined;
75
75
  }>;
76
76
  export type Input = z.infer<typeof schema>;
77
- export declare function execute(client: HonuaClient, input: Input): Promise<{
77
+ export declare function execute(client: HonuaClient, input: Input, signal?: AbortSignal): Promise<{
78
78
  content: Array<{
79
79
  type: "text";
80
80
  text: string;
@@ -1 +1 @@
1
- {"version":3,"file":"get-extent.d.ts","sourceRoot":"","sources":["../../../src/tools/get-extent.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AACjD,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAMxB;;;;;;;GAOG;AACH,eAAO,MAAM,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAGjB,CAAC;AAEH,MAAM,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,MAAM,CAAC,CAAC;AAE3C,wBAAsB,OAAO,CAAC,MAAM,EAAE,WAAW,EAAE,KAAK,EAAE,KAAK;;;;;;;;;;;;GAgB9D"}
1
+ {"version":3,"file":"get-extent.d.ts","sourceRoot":"","sources":["../../../src/tools/get-extent.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AACjD,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAMxB;;;;;;;GAOG;AACH,eAAO,MAAM,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAGjB,CAAC;AAEH,MAAM,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,MAAM,CAAC,CAAC;AAE3C,wBAAsB,OAAO,CAAC,MAAM,EAAE,WAAW,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,CAAC,EAAE,WAAW;;;;;;;;;;;;GAiBpF"}
@@ -15,10 +15,11 @@ export const schema = z.object({
15
15
  ...sourceRefFields,
16
16
  ...queryFilterFields,
17
17
  });
18
- export async function execute(client, input) {
18
+ export async function execute(client, input, signal) {
19
19
  return withCapabilityHonesty(async () => {
20
20
  const resolved = resolveSource(client, input);
21
21
  const query = toQuery(input, { protocol: resolved.descriptor.protocol, paginate: false });
22
+ query.signal = signal;
22
23
  const { extent, count } = await resolved.source.queryExtent(query);
23
24
  return jsonText({
24
25
  source: resolved.ref.ref,
@@ -111,7 +111,7 @@ export declare const schema: z.ZodObject<{
111
111
  outSr?: string | number | undefined;
112
112
  }>;
113
113
  export type Input = z.infer<typeof schema>;
114
- export declare function execute(client: HonuaClient, input: Input): Promise<{
114
+ export declare function execute(client: HonuaClient, input: Input, signal?: AbortSignal): Promise<{
115
115
  content: Array<{
116
116
  type: "text";
117
117
  text: string;
@@ -1 +1 @@
1
- {"version":3,"file":"query-features.d.ts","sourceRoot":"","sources":["../../../src/tools/query-features.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AACjD,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAOxB;;;;;;;;GAQG;AACH,eAAO,MAAM,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAIjB,CAAC;AAEH,MAAM,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,MAAM,CAAC,CAAC;AAE3C,wBAAsB,OAAO,CAAC,MAAM,EAAE,WAAW,EAAE,KAAK,EAAE,KAAK;;;;;;;;;;;;GAyB9D"}
1
+ {"version":3,"file":"query-features.d.ts","sourceRoot":"","sources":["../../../src/tools/query-features.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AACjD,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAOxB;;;;;;;;GAQG;AACH,eAAO,MAAM,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAIjB,CAAC;AAEH,MAAM,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,MAAM,CAAC,CAAC;AAE3C,wBAAsB,OAAO,CAAC,MAAM,EAAE,WAAW,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,CAAC,EAAE,WAAW;;;;;;;;;;;;GA0BpF"}
@@ -18,10 +18,11 @@ export const schema = z.object({
18
18
  ...queryFilterFields,
19
19
  ...queryShapeFields,
20
20
  });
21
- export async function execute(client, input) {
21
+ export async function execute(client, input, signal) {
22
22
  return withCapabilityHonesty(async () => {
23
23
  const resolved = resolveSource(client, input);
24
24
  const query = toQuery(input, { protocol: resolved.descriptor.protocol });
25
+ query.signal = signal;
25
26
  const result = await resolved.source.query(query);
26
27
  // Legacy addressing keeps legacy output: a client that still sends
27
28
  // serviceId/layerId also still expects Esri-JSON geometry. Neutral
@@ -84,7 +84,7 @@ export declare const schema: z.ZodObject<{
84
84
  groupBy?: string | string[] | undefined;
85
85
  }>;
86
86
  export type Input = z.infer<typeof schema>;
87
- export declare function execute(client: HonuaClient, input: Input): Promise<{
87
+ export declare function execute(client: HonuaClient, input: Input, signal?: AbortSignal): Promise<{
88
88
  content: Array<{
89
89
  type: "text";
90
90
  text: string;
@@ -1 +1 @@
1
- {"version":3,"file":"statistics.d.ts","sourceRoot":"","sources":["../../../src/tools/statistics.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAEjD,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAOxB;;;;;;;;GAQG;AACH,eAAO,MAAM,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAWjB,CAAC;AAEH,MAAM,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,MAAM,CAAC,CAAC;AAQ3C,wBAAsB,OAAO,CAAC,MAAM,EAAE,WAAW,EAAE,KAAK,EAAE,KAAK;;;;;;;;;;;;GAgC9D"}
1
+ {"version":3,"file":"statistics.d.ts","sourceRoot":"","sources":["../../../src/tools/statistics.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAEjD,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAOxB;;;;;;;;GAQG;AACH,eAAO,MAAM,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAWjB,CAAC;AAEH,MAAM,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,MAAM,CAAC,CAAC;AAQ3C,wBAAsB,OAAO,CAAC,MAAM,EAAE,WAAW,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,CAAC,EAAE,WAAW;;;;;;;;;;;;GAiCpF"}
@@ -31,10 +31,11 @@ function normalizeGroupBy(groupBy) {
31
31
  const fields = (Array.isArray(groupBy) ? groupBy : groupBy.split(",")).map((f) => f.trim()).filter(Boolean);
32
32
  return fields.length > 0 ? fields : undefined;
33
33
  }
34
- export async function execute(client, input) {
34
+ export async function execute(client, input, signal) {
35
35
  return withCapabilityHonesty(async () => {
36
36
  const resolved = resolveSource(client, input);
37
37
  const base = toQuery(input, { protocol: resolved.descriptor.protocol, paginate: false });
38
+ base.signal = signal;
38
39
  // GeoServices rejects an `outStatistics` query that carries no `where`;
39
40
  // every other protocol treats "no filter" as "all records". Supplying the
40
41
  // tautology keeps the neutral "omit the filter ⇒ aggregate everything"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@honua/mcp-server",
3
- "version": "0.1.12",
3
+ "version": "0.1.14",
4
4
  "description": "Platform-free MCP server for geospatial services: expose discovery, query, and analysis over any public ArcGIS/Esri FeatureServer or OGC API endpoint to any MCP client, with no platform lock-in",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Honua",
@@ -85,11 +85,11 @@
85
85
  "overrides": {
86
86
  "hono": "^4.13.7",
87
87
  "@hono/node-server": "^2.0.5",
88
- "fast-uri": "^3.1.6",
88
+ "fast-uri": "^3.1.8",
89
89
  "qs": "^6.16.0"
90
90
  },
91
91
  "peerDependencies": {
92
- "@honua/sdk-js": "^0.1.12"
92
+ "@honua/sdk-js": "^0.1.14"
93
93
  },
94
94
  "devDependencies": {
95
95
  "@biomejs/biome": "1.9.4",
@@ -2,7 +2,7 @@
2
2
  "mcpServers": {
3
3
  "honua": {
4
4
  "command": "npx",
5
- "args": ["-y", "--package", "@honua/mcp-server@0.1.11-beta.0", "honua-mcp-proxy"],
5
+ "args": ["-y", "--package", "@honua/mcp-server@0.1.12", "honua-mcp-proxy"],
6
6
  "env": {
7
7
  "HONUA_MCP_REMOTE_URL": "http://localhost:8080/mcp"
8
8
  }
@@ -2,7 +2,7 @@
2
2
  "mcpServers": {
3
3
  "honua": {
4
4
  "command": "npx",
5
- "args": ["-y", "--package", "@honua/mcp-server@0.1.11-beta.0", "honua-mcp-proxy"],
5
+ "args": ["-y", "--package", "@honua/mcp-server@0.1.12", "honua-mcp-proxy"],
6
6
  "env": {
7
7
  "HONUA_MCP_REMOTE_URL": "http://localhost:8080/mcp"
8
8
  }
@@ -2,7 +2,7 @@
2
2
  "mcpServers": {
3
3
  "honua": {
4
4
  "command": "npx",
5
- "args": ["-y", "--package", "@honua/mcp-server@0.1.11-beta.0", "honua-mcp-proxy"],
5
+ "args": ["-y", "--package", "@honua/mcp-server@0.1.12", "honua-mcp-proxy"],
6
6
  "env": {
7
7
  "HONUA_MCP_REMOTE_URL": "http://localhost:8080/mcp"
8
8
  }