@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 +91 -6
- package/dist/src/certification/admin-parity.js +5 -5
- package/dist/src/index.js +4 -4
- package/dist/src/neutral/result.d.ts +2 -2
- package/dist/src/neutral/result.d.ts.map +1 -1
- package/dist/src/neutral/result.js +11 -4
- package/dist/src/proxy.d.ts +30 -14
- package/dist/src/proxy.d.ts.map +1 -1
- package/dist/src/proxy.js +150 -69
- package/dist/src/tools/count-features.d.ts +1 -1
- package/dist/src/tools/count-features.d.ts.map +1 -1
- package/dist/src/tools/count-features.js +2 -1
- package/dist/src/tools/get-extent.d.ts +1 -1
- package/dist/src/tools/get-extent.d.ts.map +1 -1
- package/dist/src/tools/get-extent.js +2 -1
- package/dist/src/tools/query-features.d.ts +1 -1
- package/dist/src/tools/query-features.d.ts.map +1 -1
- package/dist/src/tools/query-features.js +2 -1
- package/dist/src/tools/statistics.d.ts +1 -1
- package/dist/src/tools/statistics.d.ts.map +1 -1
- package/dist/src/tools/statistics.js +2 -1
- package/package.json +3 -3
- package/release/zero-to-map/configs/claude-code.mcp.json +1 -1
- package/release/zero-to-map/configs/claude-desktop.json +1 -1
- package/release/zero-to-map/configs/cursor.json +1 -1
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
|
-
|
|
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
|
|
394
|
-
|
|
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
|
-
|
|
421
|
-
|
|
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 {
|
|
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
|
|
206
|
-
const
|
|
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
|
|
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
|
|
19
|
+
capability: import("@honua/sdk-js").Capability;
|
|
20
20
|
reason: string;
|
|
21
|
-
protocol: import("@honua/sdk-js
|
|
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":"
|
|
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 {
|
|
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
|
-
|
|
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);
|
package/dist/src/proxy.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
3
|
-
import {
|
|
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:
|
|
12
|
-
*
|
|
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
|
|
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
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
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
|
|
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
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
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
|
package/dist/src/proxy.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"proxy.d.ts","sourceRoot":"","sources":["../../src/proxy.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAC;
|
|
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 {
|
|
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
|
-
|
|
62
|
-
export async function connectUpstream(options) {
|
|
54
|
+
function createUpstreamTransport(options) {
|
|
63
55
|
const headers = buildUpstreamHeaders(options);
|
|
64
|
-
|
|
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(
|
|
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
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
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
|
-
|
|
79
|
-
const
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
*
|
|
121
|
-
*
|
|
122
|
-
*
|
|
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
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
-
|
|
131
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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;;;;;;;;;;;;
|
|
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;;;;;;;;;;;;
|
|
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;;;;;;;;;;;;
|
|
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;;;;;;;;;;;;
|
|
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.
|
|
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.
|
|
88
|
+
"fast-uri": "^3.1.8",
|
|
89
89
|
"qs": "^6.16.0"
|
|
90
90
|
},
|
|
91
91
|
"peerDependencies": {
|
|
92
|
-
"@honua/sdk-js": "^0.1.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
}
|