@honua/mcp-server 0.0.9-alpha.0 → 0.0.20-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (147) hide show
  1. package/README.md +167 -0
  2. package/certification/geospatial-mcp-schemas/PROVENANCE.md +33 -0
  3. package/certification/geospatial-mcp-schemas/README.md +90 -0
  4. package/certification/geospatial-mcp-schemas/common/geoprocessing-error.schema.json +45 -0
  5. package/certification/geospatial-mcp-schemas/conformance/manifest.schema.json +73 -0
  6. package/certification/geospatial-mcp-schemas/index.json +311 -0
  7. package/certification/geospatial-mcp-schemas/resources/app-package.schema.json +20 -0
  8. package/certification/geospatial-mcp-schemas/resources/app-template.schema.json +15 -0
  9. package/certification/geospatial-mcp-schemas/resources/artifact.schema.json +69 -0
  10. package/certification/geospatial-mcp-schemas/resources/deployment.schema.json +27 -0
  11. package/certification/geospatial-mcp-schemas/resources/map-package.schema.json +21 -0
  12. package/certification/geospatial-mcp-schemas/resources/map-template.schema.json +15 -0
  13. package/certification/geospatial-mcp-schemas/resources/provenance.schema.json +50 -0
  14. package/certification/geospatial-mcp-schemas/resources/published-service.schema.json +21 -0
  15. package/certification/geospatial-mcp-schemas/resources/result-package.schema.json +64 -0
  16. package/certification/geospatial-mcp-schemas/resources/style.schema.json +12 -0
  17. package/certification/geospatial-mcp-schemas/resources/theme.schema.json +12 -0
  18. package/certification/geospatial-mcp-schemas/resources/workspace.schema.json +63 -0
  19. package/certification/geospatial-mcp-schemas/tools/apply_style_preset.schema.json +23 -0
  20. package/certification/geospatial-mcp-schemas/tools/cancel_job.schema.json +15 -0
  21. package/certification/geospatial-mcp-schemas/tools/clarify_intent.schema.json +69 -0
  22. package/certification/geospatial-mcp-schemas/tools/compose_mixed_protocol_map.schema.json +21 -0
  23. package/certification/geospatial-mcp-schemas/tools/create_app_package.schema.json +33 -0
  24. package/certification/geospatial-mcp-schemas/tools/create_map_package.schema.json +41 -0
  25. package/certification/geospatial-mcp-schemas/tools/execute_plan.schema.json +64 -0
  26. package/certification/geospatial-mcp-schemas/tools/geocode_address.schema.json +36 -0
  27. package/certification/geospatial-mcp-schemas/tools/get_style.schema.json +30 -0
  28. package/certification/geospatial-mcp-schemas/tools/ground_candidates.schema.json +66 -0
  29. package/certification/geospatial-mcp-schemas/tools/list_capabilities.schema.json +21 -0
  30. package/certification/geospatial-mcp-schemas/tools/list_layers.schema.json +14 -0
  31. package/certification/geospatial-mcp-schemas/tools/plan_analysis.schema.json +19 -0
  32. package/certification/geospatial-mcp-schemas/tools/preview_app_package.schema.json +15 -0
  33. package/certification/geospatial-mcp-schemas/tools/preview_map_package.schema.json +17 -0
  34. package/certification/geospatial-mcp-schemas/tools/preview_package.schema.json +40 -0
  35. package/certification/geospatial-mcp-schemas/tools/propose_operation.schema.json +28 -0
  36. package/certification/geospatial-mcp-schemas/tools/publish_result.schema.json +29 -0
  37. package/certification/geospatial-mcp-schemas/tools/publish_service.schema.json +59 -0
  38. package/certification/geospatial-mcp-schemas/tools/query_features.schema.json +54 -0
  39. package/certification/geospatial-mcp-schemas/tools/refine_map_package.schema.json +22 -0
  40. package/certification/geospatial-mcp-schemas/tools/render_map.schema.json +55 -0
  41. package/certification/geospatial-mcp-schemas/tools/resolve_entity.schema.json +30 -0
  42. package/certification/geospatial-mcp-schemas/tools/solve_route.schema.json +39 -0
  43. package/certification/geospatial-mcp-schemas/tools/validate_package.schema.json +35 -0
  44. package/certification/geospatial-mcp-schemas/tools/validate_plan.schema.json +61 -0
  45. package/dist/src/certification/certifier.d.ts +119 -0
  46. package/dist/src/certification/certifier.d.ts.map +1 -0
  47. package/dist/src/certification/certifier.js +410 -0
  48. package/dist/src/certification/cli.d.ts +3 -0
  49. package/dist/src/certification/cli.d.ts.map +1 -0
  50. package/dist/src/certification/cli.js +48 -0
  51. package/dist/src/certification/contracts.d.ts +62 -0
  52. package/dist/src/certification/contracts.d.ts.map +1 -0
  53. package/dist/src/certification/contracts.js +174 -0
  54. package/dist/src/certification/fixture-client.d.ts +4 -0
  55. package/dist/src/certification/fixture-client.d.ts.map +1 -0
  56. package/dist/src/certification/fixture-client.js +113 -0
  57. package/dist/src/certification/fixtures.d.ts +32 -0
  58. package/dist/src/certification/fixtures.d.ts.map +1 -0
  59. package/dist/src/certification/fixtures.js +64 -0
  60. package/dist/src/certification/json-schema.d.ts +49 -0
  61. package/dist/src/certification/json-schema.d.ts.map +1 -0
  62. package/dist/src/certification/json-schema.js +164 -0
  63. package/dist/src/certification/mock-upstream.d.ts +29 -0
  64. package/dist/src/certification/mock-upstream.d.ts.map +1 -0
  65. package/dist/src/certification/mock-upstream.js +96 -0
  66. package/dist/src/certification/operator-catalog.d.ts +82 -0
  67. package/dist/src/certification/operator-catalog.d.ts.map +1 -0
  68. package/dist/src/certification/operator-catalog.js +466 -0
  69. package/dist/src/certification/report.d.ts +4 -0
  70. package/dist/src/certification/report.d.ts.map +1 -0
  71. package/dist/src/certification/report.js +90 -0
  72. package/dist/src/certification/run.d.ts +28 -0
  73. package/dist/src/certification/run.d.ts.map +1 -0
  74. package/dist/src/certification/run.js +31 -0
  75. package/dist/src/certification/schema-index.d.ts +69 -0
  76. package/dist/src/certification/schema-index.d.ts.map +1 -0
  77. package/dist/src/certification/schema-index.js +82 -0
  78. package/dist/src/certification/target.d.ts +40 -0
  79. package/dist/src/certification/target.d.ts.map +1 -0
  80. package/dist/src/certification/target.js +167 -0
  81. package/dist/src/eval/cli.d.ts +3 -0
  82. package/dist/src/eval/cli.d.ts.map +1 -0
  83. package/dist/src/eval/cli.js +116 -0
  84. package/dist/src/eval/corpus.d.ts +27 -0
  85. package/dist/src/eval/corpus.d.ts.map +1 -0
  86. package/dist/src/eval/corpus.js +134 -0
  87. package/dist/src/eval/drivers/anthropic.d.ts +14 -0
  88. package/dist/src/eval/drivers/anthropic.d.ts.map +1 -0
  89. package/dist/src/eval/drivers/anthropic.js +120 -0
  90. package/dist/src/eval/drivers/bedrock.d.ts +54 -0
  91. package/dist/src/eval/drivers/bedrock.d.ts.map +1 -0
  92. package/dist/src/eval/drivers/bedrock.js +178 -0
  93. package/dist/src/eval/drivers/deterministic.d.ts +17 -0
  94. package/dist/src/eval/drivers/deterministic.d.ts.map +1 -0
  95. package/dist/src/eval/drivers/deterministic.js +51 -0
  96. package/dist/src/eval/drivers/index.d.ts +34 -0
  97. package/dist/src/eval/drivers/index.d.ts.map +1 -0
  98. package/dist/src/eval/drivers/index.js +74 -0
  99. package/dist/src/eval/drivers/openai.d.ts +14 -0
  100. package/dist/src/eval/drivers/openai.d.ts.map +1 -0
  101. package/dist/src/eval/drivers/openai.js +136 -0
  102. package/dist/src/eval/grade.d.ts +14 -0
  103. package/dist/src/eval/grade.d.ts.map +1 -0
  104. package/dist/src/eval/grade.js +56 -0
  105. package/dist/src/eval/northstar-corpus.d.ts +3 -0
  106. package/dist/src/eval/northstar-corpus.d.ts.map +1 -0
  107. package/dist/src/eval/northstar-corpus.js +194 -0
  108. package/dist/src/eval/operator-corpus.d.ts +3 -0
  109. package/dist/src/eval/operator-corpus.d.ts.map +1 -0
  110. package/dist/src/eval/operator-corpus.js +193 -0
  111. package/dist/src/eval/report.d.ts +104 -0
  112. package/dist/src/eval/report.d.ts.map +1 -0
  113. package/dist/src/eval/report.js +130 -0
  114. package/dist/src/eval/runner.d.ts +22 -0
  115. package/dist/src/eval/runner.d.ts.map +1 -0
  116. package/dist/src/eval/runner.js +116 -0
  117. package/dist/src/eval/types.d.ts +93 -0
  118. package/dist/src/eval/types.d.ts.map +1 -0
  119. package/dist/src/eval/types.js +10 -0
  120. package/dist/src/helpers.d.ts +11 -0
  121. package/dist/src/helpers.d.ts.map +1 -1
  122. package/dist/src/helpers.js +22 -0
  123. package/dist/src/index.d.ts +7 -0
  124. package/dist/src/index.d.ts.map +1 -1
  125. package/dist/src/index.js +30 -4
  126. package/dist/src/proxy.d.ts +46 -0
  127. package/dist/src/proxy.d.ts.map +1 -0
  128. package/dist/src/proxy.js +124 -0
  129. package/dist/src/resources/styles.d.ts +29 -0
  130. package/dist/src/resources/styles.d.ts.map +1 -0
  131. package/dist/src/resources/styles.js +43 -0
  132. package/dist/src/styles.d.ts +110 -0
  133. package/dist/src/styles.d.ts.map +1 -0
  134. package/dist/src/styles.js +118 -0
  135. package/dist/src/tools/apply-style-preset.d.ts +27 -0
  136. package/dist/src/tools/apply-style-preset.d.ts.map +1 -0
  137. package/dist/src/tools/apply-style-preset.js +29 -0
  138. package/dist/src/tools/count-features.d.ts.map +1 -1
  139. package/dist/src/tools/count-features.js +5 -4
  140. package/dist/src/tools/get-extent.d.ts.map +1 -1
  141. package/dist/src/tools/get-extent.js +6 -4
  142. package/dist/src/tools/get-style.d.ts +24 -0
  143. package/dist/src/tools/get-style.d.ts.map +1 -0
  144. package/dist/src/tools/get-style.js +28 -0
  145. package/dist/src/tools/query-features.d.ts.map +1 -1
  146. package/dist/src/tools/query-features.js +4 -1
  147. package/package.json +19 -4
@@ -0,0 +1,193 @@
1
+ /**
2
+ * Operator-surface corpus for the cross-model eval (honua-server #1956).
3
+ *
4
+ * The analyst corpus in `corpus.ts` targets the SDK `@honua/mcp-server` ANALYST
5
+ * tools (count_features, describe_layer, query_features, …). This corpus instead
6
+ * exercises the LIVE honua-server OPERATOR MCP surface — the agent-native control
7
+ * plane published at `https://demo.honua.io/mcp` (MCP streamable-HTTP, protocol
8
+ * 2025-03-26, serverInfo `honua.operator.mcp`). Its tools are the grounding →
9
+ * clarify → plan → validate → dry-run → (gated) execute lifecycle plus read-only
10
+ * data tools (list layers, query features) and Pro tools (geocode, route).
11
+ *
12
+ * Select it with `HONUA_EVAL_CORPUS=operator` (see `resolveCorpus` in corpus.ts)
13
+ * and run live with `--driver bedrock` + `HONUA_MCP_REMOTE_URL=<demo>/mcp` +
14
+ * `HONUA_API_KEY=<demo key>`. The same grading runs for every driver, so the
15
+ * deterministic control and Bedrock-Claude are measured against identical
16
+ * criteria over the identical live catalog.
17
+ *
18
+ * SAFETY: these scenarios are READ-MOSTLY and reversible. The end-to-end planning
19
+ * scenarios stop at validate / dry-run and FORBID the mutating lifecycle tools
20
+ * (`honua_execute_plan`, `honua_propose_operation`) — on the Community-edition
21
+ * demo `honua_propose_operation` executes directly (no approval gate), so a
22
+ * correct agent must NOT call it for a read-only analysis task. Forbidding those
23
+ * tools turns "did the agent stay read-only?" into a graded criterion.
24
+ *
25
+ * Grading is deliberately tool-centric: required-tool selection, ordered
26
+ * subsequence, and forbidden-tool avoidance — the verifiable signals of "the
27
+ * client composed the right workflow". Answer-content checks are used only where
28
+ * a data value (a layer name, a geometry word) is near-certain to be echoed, so a
29
+ * non-pass means a real capability/composition gap rather than a phrasing
30
+ * artifact. Tools gated by edition entitlements (geocode/route) are scored on
31
+ * correct tool selection alone (see `operator-geocode-gap`).
32
+ *
33
+ * Dataset shapes resolve against the seeded Maui demo content: the published
34
+ * service `svc-publish-maui-buildings` (layer 13, Overture building footprints).
35
+ */
36
+ /** Seeded demo service/layer the scenarios bind to (from honua_list_layers). */
37
+ const MAUI_BUILDINGS_SERVICE = "svc-publish-maui-buildings";
38
+ const MAUI_BUILDINGS_LAYER = 13;
39
+ /** Mutating lifecycle tools a read-only analysis agent must never call. */
40
+ const MUTATING_TOOLS = ["honua_execute_plan", "honua_propose_operation"];
41
+ /** A small, static, well-formed analysis plan reused by validate/dry-run. */
42
+ const SAMPLE_PLAN = {
43
+ planId: "eval-operator-plan-1",
44
+ intentId: "eval-operator-intent-1",
45
+ steps: [
46
+ {
47
+ stepId: "s1",
48
+ kind: "QueryFeatures",
49
+ inputs: { serviceId: MAUI_BUILDINGS_SERVICE, layerId: String(MAUI_BUILDINGS_LAYER) },
50
+ },
51
+ ],
52
+ outputs: ["Scalar"],
53
+ };
54
+ export const OPERATOR_CORPUS = [
55
+ {
56
+ id: "operator-list-layers",
57
+ title: "Discover the published layers on the operator surface",
58
+ category: "discovery",
59
+ prompt: "I just connected to this Honua server. What map layers are published here that I could query or map? Just list what's available.",
60
+ criteria: {
61
+ requiredTools: ["honua_list_layers"],
62
+ answerMustInclude: ["maui"],
63
+ },
64
+ script: [{ tool: "honua_list_layers", args: {} }],
65
+ },
66
+ {
67
+ id: "operator-discover-then-query",
68
+ title: "Discover a layer, then query features from it",
69
+ category: "query",
70
+ prompt: "Find the Maui buildings layer on this server and show me a few of its building features so I can see what attributes they carry.",
71
+ criteria: {
72
+ requiredTools: ["honua_list_layers", "honua_query_features"],
73
+ expectedToolSequence: ["honua_list_layers", "honua_query_features"],
74
+ forbiddenTools: MUTATING_TOOLS,
75
+ answerMustInclude: ["building"],
76
+ },
77
+ script: [
78
+ { tool: "honua_list_layers", args: { filter: "maui-buildings" } },
79
+ {
80
+ tool: "honua_query_features",
81
+ args: { serviceId: MAUI_BUILDINGS_SERVICE, layerId: MAUI_BUILDINGS_LAYER, limit: 3 },
82
+ },
83
+ ],
84
+ },
85
+ {
86
+ id: "operator-ground-intent",
87
+ title: "Ground an ambiguous analysis goal to a workflow",
88
+ category: "grounding",
89
+ prompt: "I want to count how many Maui buildings fall within 100 meters of the coast. Help me get started — figure out what kind of workflow this is and what data and processes apply. Don't run anything yet.",
90
+ criteria: {
91
+ requiredTools: ["honua_ground_candidates"],
92
+ forbiddenTools: MUTATING_TOOLS,
93
+ },
94
+ script: [
95
+ {
96
+ tool: "honua_ground_candidates",
97
+ args: {
98
+ goal: "count how many Maui buildings fall within 100 meters of the coast",
99
+ assumptionPolicy: "AskAlways",
100
+ },
101
+ },
102
+ ],
103
+ },
104
+ {
105
+ id: "operator-clarify-loop",
106
+ title: "Ground then answer the clarification to refine intent",
107
+ category: "multi-step",
108
+ prompt: "I want to buffer the Maui buildings by 100 meters and count how many features fall in the buffered area. Start the grounding pass, and when the server asks a clarifying question, answer it (the buffer distance is 100 meters, the input is the maui-buildings layer). Stop once the intent is refined — do not execute.",
109
+ criteria: {
110
+ requiredTools: ["honua_ground_candidates", "honua_clarify_intent"],
111
+ expectedToolSequence: ["honua_ground_candidates", "honua_clarify_intent"],
112
+ forbiddenTools: MUTATING_TOOLS,
113
+ },
114
+ script: [
115
+ {
116
+ tool: "honua_ground_candidates",
117
+ args: {
118
+ goal: "buffer the maui buildings by 100 meters and count how many fall in the area",
119
+ assumptionPolicy: "AskAlways",
120
+ },
121
+ },
122
+ {
123
+ tool: "honua_clarify_intent",
124
+ args: {
125
+ goal: "buffer the maui buildings by 100 meters and count how many fall in the area",
126
+ intentId: "eval-operator-clarify-1",
127
+ response: { answers: { "param.distance": ["100"], "param.input": ["maui-buildings"] } },
128
+ },
129
+ },
130
+ ],
131
+ },
132
+ {
133
+ id: "operator-plan-validate",
134
+ title: "End-to-end read-only planning: ground → plan → validate",
135
+ category: "multi-step",
136
+ prompt: "I need an executable analysis plan to count the Maui buildings, but I am NOT ready to run it. Ground the goal, compile it into an analysis plan, and validate that the plan is executable and what approvals it would need. Do not execute or propose any operation.",
137
+ criteria: {
138
+ requiredTools: ["honua_ground_candidates", "honua_plan_analysis", "honua_validate_plan"],
139
+ expectedToolSequence: ["honua_ground_candidates", "honua_plan_analysis", "honua_validate_plan"],
140
+ forbiddenTools: MUTATING_TOOLS,
141
+ },
142
+ script: [
143
+ { tool: "honua_ground_candidates", args: { goal: "count the maui buildings", assumptionPolicy: "UseDefaults" } },
144
+ { tool: "honua_plan_analysis", args: { intent: "count the features in the maui-buildings layer" } },
145
+ { tool: "honua_validate_plan", args: { plan: SAMPLE_PLAN } },
146
+ ],
147
+ },
148
+ {
149
+ id: "operator-dry-run",
150
+ title: "Estimate a plan with a dry run before executing",
151
+ category: "analysis",
152
+ prompt: "Before I commit to running an analysis that queries the Maui buildings layer, give me a dry-run estimate of how long it would take, what kind of artifacts it would produce, and any side effects. Do not actually execute it.",
153
+ criteria: {
154
+ requiredTools: ["honua_dry_run_plan"],
155
+ forbiddenTools: MUTATING_TOOLS,
156
+ },
157
+ script: [{ tool: "honua_dry_run_plan", args: { plan: SAMPLE_PLAN } }],
158
+ },
159
+ {
160
+ id: "operator-validate-package",
161
+ title: "Validate a query package against the governance contract",
162
+ category: "governance",
163
+ prompt: 'Run a read-only validation/review of a query package and tell me whether it is ready to execute and whether it needs approval. The package family is \'query\' and its entire payload is just this layer binding: { "serviceId": "svc-publish-maui-buildings", "layerId": 13 }. Validate it exactly as given — do not ask me for anything more.',
164
+ criteria: {
165
+ requiredTools: ["honua_validate_package"],
166
+ forbiddenTools: MUTATING_TOOLS,
167
+ },
168
+ script: [
169
+ {
170
+ tool: "honua_validate_package",
171
+ args: {
172
+ packageFamily: "query",
173
+ packagePayload: { serviceId: MAUI_BUILDINGS_SERVICE, layerId: MAUI_BUILDINGS_LAYER },
174
+ },
175
+ },
176
+ ],
177
+ },
178
+ {
179
+ id: "operator-geocode-gap",
180
+ title: "Route an address task to geocoding (entitlement-gated)",
181
+ category: "capability-gap",
182
+ prompt: "I have a street address — 'Kahului, Maui, Hawaii'. Get me the coordinates for it using this server. If the server can't do it, tell me honestly why instead of guessing coordinates.",
183
+ criteria: {
184
+ // Pro-gated on the Community demo: scored on correct tool SELECTION (the
185
+ // client must route an address task to the geocoder rather than fabricate
186
+ // coordinates). The honest gap report is asserted qualitatively, not via a
187
+ // brittle answer-substring match. forbiddenTools keeps it read-only.
188
+ requiredTools: ["honua_geocode_address"],
189
+ forbiddenTools: MUTATING_TOOLS,
190
+ },
191
+ script: [{ tool: "honua_geocode_address", args: { address: "Kahului, Maui, Hawaii", maxResults: 3 } }],
192
+ },
193
+ ];
@@ -0,0 +1,104 @@
1
+ import type { ModelDriver, Scenario, ScenarioGrade, WorkflowTranscript } from "./types.js";
2
+ /** One graded (model, scenario) result plus a compact transcript view. */
3
+ export interface EvalResult {
4
+ scenarioId: string;
5
+ modelId: string;
6
+ outcome: ScenarioGrade["outcome"];
7
+ violations: string[];
8
+ toolsCalled: string[];
9
+ errorCount: number;
10
+ driverError?: string | undefined;
11
+ /**
12
+ * Required tools THIS scenario needs that the live `tools/list` did not
13
+ * advertise — the per-workflow view of {@link EvalReport.catalog.unresolvedRequiredTools}.
14
+ * Empty when the surface advertises every tool the scenario requires. This is
15
+ * the P1 gate dashboard: it names, per north-star workflow, exactly which tools
16
+ * a target surface is still missing.
17
+ */
18
+ missingTools: string[];
19
+ }
20
+ /**
21
+ * Auth mode the eval used to reach the MCP surface.
22
+ *
23
+ * - `bearer` — `Authorization: Bearer <token>` (HONUA_MCP_AUTH_TOKEN).
24
+ * - `api-key` — `x-api-key: <key>` (HONUA_API_KEY).
25
+ * - `anonymous` — a live remote surface reached with NO credential.
26
+ * - `none` — the in-process offline fixture surface (no transport auth).
27
+ *
28
+ * Recorded in the artifact so a published live run proves it was authenticated
29
+ * (not run through a dev-auth bypass).
30
+ */
31
+ export type AuthMode = "bearer" | "api-key" | "anonymous" | "none";
32
+ /** Per-model aggregate metrics — the measurable form of the north star. */
33
+ export interface ModelScorecard {
34
+ id: string;
35
+ /** Resolved model id for live drivers (attributable); omitted for the control. */
36
+ model?: string;
37
+ vendor: ModelDriver["vendor"];
38
+ available: boolean;
39
+ scenarios: number;
40
+ pass: number;
41
+ fail: number;
42
+ clarified: number;
43
+ error: number;
44
+ /** pass / scenarios. */
45
+ successRate: number;
46
+ /** clarified / scenarios. */
47
+ clarificationRate: number;
48
+ /** scenarios with >=1 erroring tool call / scenarios. */
49
+ editRate: number;
50
+ totalToolErrors: number;
51
+ }
52
+ export interface EvalReport {
53
+ schemaVersion: 4;
54
+ generatedAt: string;
55
+ surface: {
56
+ backend: "fixture" | "live";
57
+ mcpTransport: string;
58
+ remoteUrl?: string;
59
+ /** How the eval authenticated to the surface (proves the run was authed). */
60
+ auth: AuthMode;
61
+ };
62
+ corpus: {
63
+ scenarios: number;
64
+ ids: string[];
65
+ };
66
+ /**
67
+ * Catalog-coverage audit: how the corpus's required tools resolve against the
68
+ * LIVE `tools/list` the surface advertised. `unresolvedRequiredTools` names any
69
+ * required tool absent from the live catalog — so a scenario that fails does so
70
+ * for a real capability gap, not a silent name-resolution bug.
71
+ */
72
+ catalog: {
73
+ advertisedToolCount: number;
74
+ requiredTools: string[];
75
+ unresolvedRequiredTools: string[];
76
+ };
77
+ models: ModelScorecard[];
78
+ results: EvalResult[];
79
+ summary: {
80
+ /** True iff the deterministic control passed every scenario (the CI gate). */
81
+ pass: boolean;
82
+ modelsEvaluated: number;
83
+ liveModelsEvaluated: number;
84
+ scenarios: number;
85
+ };
86
+ }
87
+ export interface AssembleInput {
88
+ backend: "fixture" | "live";
89
+ mcpTransport: string;
90
+ remoteUrl?: string | undefined;
91
+ auth: AuthMode;
92
+ /** Tool names advertised by the live `tools/list` (for the catalog audit). */
93
+ advertisedTools: string[];
94
+ corpus: Scenario[];
95
+ drivers: ModelDriver[];
96
+ graded: {
97
+ grade: ScenarioGrade;
98
+ transcript: WorkflowTranscript;
99
+ }[];
100
+ }
101
+ export declare function assembleReport(input: AssembleInput): EvalReport;
102
+ /** Render a human-readable Markdown summary of the eval report. */
103
+ export declare function renderMarkdown(report: EvalReport): string;
104
+ //# sourceMappingURL=report.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"report.d.ts","sourceRoot":"","sources":["../../../src/eval/report.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,130 @@
1
+ /** Distinct required-tool names referenced across a corpus. */
2
+ function corpusRequiredTools(corpus) {
3
+ const names = new Set();
4
+ for (const scenario of corpus) {
5
+ for (const tool of scenario.criteria.requiredTools) {
6
+ names.add(tool);
7
+ }
8
+ }
9
+ return [...names].sort();
10
+ }
11
+ export function assembleReport(input) {
12
+ const advertised = new Set(input.advertisedTools);
13
+ // Per-scenario required tools, so each result can name the tools THIS workflow
14
+ // is missing from the surface (the per-workflow gate view).
15
+ const requiredByScenario = new Map(input.corpus.map((s) => [s.id, s.criteria.requiredTools]));
16
+ const results = input.graded.map(({ grade, transcript }) => ({
17
+ scenarioId: grade.scenarioId,
18
+ modelId: grade.modelId,
19
+ outcome: grade.outcome,
20
+ violations: grade.violations,
21
+ toolsCalled: transcript.steps.map((s) => s.tool),
22
+ errorCount: grade.errorCount,
23
+ driverError: transcript.driverError,
24
+ missingTools: (requiredByScenario.get(grade.scenarioId) ?? []).filter((t) => !advertised.has(t)),
25
+ }));
26
+ const models = input.drivers.map((driver) => {
27
+ const own = results.filter((r) => r.modelId === driver.id);
28
+ const scenarios = own.length;
29
+ const pass = own.filter((r) => r.outcome === "pass").length;
30
+ const fail = own.filter((r) => r.outcome === "fail").length;
31
+ const clarified = own.filter((r) => r.outcome === "clarified").length;
32
+ const error = own.filter((r) => r.outcome === "error").length;
33
+ const withToolErrors = own.filter((r) => r.errorCount > 0).length;
34
+ const totalToolErrors = own.reduce((acc, r) => acc + r.errorCount, 0);
35
+ const rate = (n) => (scenarios === 0 ? 0 : Number((n / scenarios).toFixed(4)));
36
+ return {
37
+ id: driver.id,
38
+ // The control has no underlying model; live drivers' id IS the resolved model.
39
+ ...(driver.vendor === "deterministic" ? {} : { model: driver.id }),
40
+ vendor: driver.vendor,
41
+ available: driver.isAvailable(),
42
+ scenarios,
43
+ pass,
44
+ fail,
45
+ clarified,
46
+ error,
47
+ successRate: rate(pass),
48
+ clarificationRate: rate(clarified),
49
+ editRate: rate(withToolErrors),
50
+ totalToolErrors,
51
+ };
52
+ });
53
+ const control = models.find((m) => m.vendor === "deterministic");
54
+ const controlPass = control ? control.fail === 0 && control.error === 0 && control.scenarios > 0 : false;
55
+ const liveModelsEvaluated = models.filter((m) => m.vendor !== "deterministic" && m.scenarios > 0).length;
56
+ const requiredTools = corpusRequiredTools(input.corpus);
57
+ const unresolvedRequiredTools = requiredTools.filter((t) => !advertised.has(t));
58
+ return {
59
+ schemaVersion: 4,
60
+ generatedAt: new Date().toISOString(),
61
+ surface: {
62
+ backend: input.backend,
63
+ mcpTransport: input.mcpTransport,
64
+ remoteUrl: input.remoteUrl,
65
+ auth: input.auth,
66
+ },
67
+ corpus: { scenarios: input.corpus.length, ids: input.corpus.map((s) => s.id) },
68
+ catalog: {
69
+ advertisedToolCount: input.advertisedTools.length,
70
+ requiredTools,
71
+ unresolvedRequiredTools,
72
+ },
73
+ models,
74
+ results,
75
+ summary: {
76
+ pass: controlPass,
77
+ modelsEvaluated: models.filter((m) => m.scenarios > 0).length,
78
+ liveModelsEvaluated,
79
+ scenarios: input.corpus.length,
80
+ },
81
+ };
82
+ }
83
+ /** Render a human-readable Markdown summary of the eval report. */
84
+ export function renderMarkdown(report) {
85
+ const lines = [];
86
+ const status = report.summary.pass ? "✅ PASS" : "❌ FAIL";
87
+ lines.push("# MCP Cross-Model Workflow Eval");
88
+ lines.push("");
89
+ lines.push(`**Result (deterministic control):** ${status}`);
90
+ lines.push("");
91
+ lines.push(`- Generated: \`${report.generatedAt}\``);
92
+ lines.push(`- MCP surface: \`${report.surface.backend}\` (transport: \`${report.surface.mcpTransport}\`${report.surface.remoteUrl ? `, remote: \`${report.surface.remoteUrl}\`` : ""})`);
93
+ lines.push(`- Auth mode: \`${report.surface.auth}\``);
94
+ lines.push(`- Corpus: ${report.corpus.scenarios} GIS workflows`);
95
+ lines.push(`- Live models evaluated: ${report.summary.liveModelsEvaluated}`);
96
+ const unresolved = report.catalog.unresolvedRequiredTools;
97
+ lines.push(unresolved.length === 0
98
+ ? `- Catalog coverage: all ${report.catalog.requiredTools.length} required tools resolve against the live catalog (${report.catalog.advertisedToolCount} advertised)`
99
+ : `- Catalog coverage: ⚠️ ${unresolved.length} required tool(s) NOT advertised by the surface: ${unresolved.map((t) => `\`${t}\``).join(", ")}`);
100
+ lines.push("");
101
+ lines.push("## Per-model scorecard");
102
+ lines.push("");
103
+ lines.push("| Model | Vendor | Avail | Pass | Fail | Clarified | Error | Success | Clarify | Edit |");
104
+ lines.push("| --- | --- | :---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |");
105
+ for (const m of report.models) {
106
+ lines.push(`| \`${m.id}\` | ${m.vendor} | ${m.available ? "yes" : "no"} | ${m.pass} | ${m.fail} | ${m.clarified} | ${m.error} | ${pct(m.successRate)} | ${pct(m.clarificationRate)} | ${pct(m.editRate)} |`);
107
+ }
108
+ lines.push("");
109
+ const failures = report.results.filter((r) => r.outcome === "fail" || r.outcome === "error");
110
+ if (failures.length > 0) {
111
+ lines.push("## Non-passing results");
112
+ lines.push("");
113
+ for (const r of failures) {
114
+ lines.push(`- \`${r.modelId}\` × \`${r.scenarioId}\` — **${r.outcome}**`);
115
+ if (r.missingTools.length > 0) {
116
+ lines.push(` - missing required tool(s) on this surface: ${r.missingTools.map((t) => `\`${t}\``).join(", ")}`);
117
+ }
118
+ for (const v of r.violations) {
119
+ lines.push(` - ${v}`);
120
+ }
121
+ }
122
+ lines.push("");
123
+ }
124
+ lines.push("> Live cross-model runs (Claude + GPT) are tracked under honua-io/honua-server#1956.");
125
+ lines.push("");
126
+ return `${lines.join("\n")}\n`;
127
+ }
128
+ function pct(rate) {
129
+ return `${(rate * 100).toFixed(0)}%`;
130
+ }
@@ -0,0 +1,22 @@
1
+ import { type EvalReport } from "./report.js";
2
+ import type { ModelDriver, Scenario } from "./types.js";
3
+ /**
4
+ * Cross-model eval runner (honua-server #1956).
5
+ *
6
+ * Backend selection mirrors the certifier: offline-first and deterministic.
7
+ * - HONUA_MCP_REMOTE_URL set → drive the live remote /mcp over streamable HTTP
8
+ * (the same surface the stdio proxy bridges).
9
+ * - otherwise → the offline in-memory fixture server (no network, no models).
10
+ *
11
+ * Drivers default to whatever `resolveDrivers` returns: the deterministic control
12
+ * always, plus Claude/GPT when their API keys are present.
13
+ */
14
+ export interface RunEvalOptions {
15
+ env?: NodeJS.ProcessEnv;
16
+ drivers?: ModelDriver[];
17
+ corpus?: Scenario[];
18
+ /** Force the in-memory fixture surface even if HONUA_MCP_REMOTE_URL is set. */
19
+ forceOffline?: boolean;
20
+ }
21
+ export declare function runEval(options?: RunEvalOptions): Promise<EvalReport>;
22
+ //# sourceMappingURL=runner.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runner.d.ts","sourceRoot":"","sources":["../../../src/eval/runner.ts"],"names":[],"mappings":"AAQA,OAAO,EAAiB,KAAK,UAAU,EAAkB,MAAM,aAAa,CAAC;AAC7E,OAAO,KAAK,EAAE,WAAW,EAAE,QAAQ,EAAmC,MAAM,YAAY,CAAC;AAEzF;;;;;;;;;;GAUG;AAEH,MAAM,WAAW,cAAc;IAC7B,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,OAAO,CAAC,EAAE,WAAW,EAAE,CAAC;IACxB,MAAM,CAAC,EAAE,QAAQ,EAAE,CAAC;IACpB,+EAA+E;IAC/E,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB;AAyFD,wBAAsB,OAAO,CAAC,OAAO,GAAE,cAAmB,GAAG,OAAO,CAAC,UAAU,CAAC,CAqC/E"}
@@ -0,0 +1,116 @@
1
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
2
+ import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
3
+ import { createFixtureClient } from "../certification/fixture-client.js";
4
+ import { createServer } from "../index.js";
5
+ import { connectUpstream, resolveProxyOptions } from "../proxy.js";
6
+ import { resolveCorpus } from "./corpus.js";
7
+ import { resolveDrivers } from "./drivers/index.js";
8
+ import { grade } from "./grade.js";
9
+ import { assembleReport } from "./report.js";
10
+ /** Truthy-flag parse shared with the driver opt-ins (0/false/no/off ⇒ false). */
11
+ function isTruthy(value) {
12
+ if (typeof value !== "string") {
13
+ return false;
14
+ }
15
+ const v = value.trim().toLowerCase();
16
+ return v.length > 0 && v !== "0" && v !== "false" && v !== "no" && v !== "off";
17
+ }
18
+ async function connectSurface(env, forceOffline) {
19
+ const live = !forceOffline && typeof env.HONUA_MCP_REMOTE_URL === "string" && env.HONUA_MCP_REMOTE_URL.length > 0;
20
+ if (live) {
21
+ const options = resolveProxyOptions(env);
22
+ // The live lane authenticates via the proxy's real credential headers — no
23
+ // dev-auth bypass. `Bearer` takes precedence over `x-api-key`; anonymous means
24
+ // no credential was supplied at all.
25
+ const auth = options.authToken ? "bearer" : options.apiKey ? "api-key" : "anonymous";
26
+ if (auth === "anonymous" && isTruthy(env.HONUA_EVAL_REQUIRE_AUTH)) {
27
+ throw new Error("HONUA_EVAL_REQUIRE_AUTH is set but the live MCP surface would be reached anonymously " +
28
+ "(no HONUA_MCP_AUTH_TOKEN / HONUA_API_KEY). Refusing to run an unauthenticated live eval.");
29
+ }
30
+ const client = await connectUpstream(options);
31
+ return {
32
+ client,
33
+ backend: "live",
34
+ mcpTransport: "streamable-http",
35
+ remoteUrl: options.remoteUrl,
36
+ auth,
37
+ close: async () => {
38
+ await client.close().catch(() => { });
39
+ },
40
+ };
41
+ }
42
+ const server = createServer(createFixtureClient());
43
+ const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
44
+ const client = new Client({ name: "honua-mcp-eval", version: "1.0.0" });
45
+ await server.connect(serverTransport);
46
+ await client.connect(clientTransport);
47
+ return {
48
+ client,
49
+ backend: "fixture",
50
+ mcpTransport: "in-memory",
51
+ auth: "none",
52
+ close: async () => {
53
+ await client.close().catch(() => { });
54
+ await server.close().catch(() => { });
55
+ },
56
+ };
57
+ }
58
+ function flattenToolResult(result) {
59
+ const r = result;
60
+ const isError = r.isError === true;
61
+ const texts = (r.content ?? [])
62
+ .filter((b) => b.type === "text" && typeof b.text === "string")
63
+ .map((b) => b.text);
64
+ const text = texts.length > 0 ? texts.join("\n") : JSON.stringify(r.content ?? result);
65
+ return { isError, text };
66
+ }
67
+ function buildContext(client, tools) {
68
+ return {
69
+ tools,
70
+ async callTool(name, args) {
71
+ try {
72
+ const result = await client.callTool({ name, arguments: args });
73
+ return flattenToolResult(result);
74
+ }
75
+ catch (err) {
76
+ return { isError: true, text: err instanceof Error ? err.message : String(err) };
77
+ }
78
+ },
79
+ };
80
+ }
81
+ export async function runEval(options = {}) {
82
+ const env = options.env ?? process.env;
83
+ const corpus = options.corpus ?? resolveCorpus(env);
84
+ const drivers = options.drivers ?? resolveDrivers({ env });
85
+ const forceOffline = options.forceOffline ?? false;
86
+ const surface = await connectSurface(env, forceOffline);
87
+ try {
88
+ const listed = await surface.client.listTools();
89
+ const tools = listed.tools.map((t) => ({
90
+ name: t.name,
91
+ description: t.description ?? "",
92
+ inputSchema: t.inputSchema,
93
+ }));
94
+ const ctx = buildContext(surface.client, tools);
95
+ const graded = [];
96
+ for (const driver of drivers) {
97
+ for (const scenario of corpus) {
98
+ const transcript = await driver.runWorkflow(scenario, ctx);
99
+ graded.push({ grade: grade(scenario, transcript), transcript });
100
+ }
101
+ }
102
+ return assembleReport({
103
+ backend: surface.backend,
104
+ mcpTransport: surface.mcpTransport,
105
+ remoteUrl: surface.remoteUrl,
106
+ auth: surface.auth,
107
+ advertisedTools: tools.map((t) => t.name),
108
+ corpus,
109
+ drivers,
110
+ graded,
111
+ });
112
+ }
113
+ finally {
114
+ await surface.close();
115
+ }
116
+ }
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Cross-model MCP workflow eval — shared types (honua-server #1956, WS-H).
3
+ *
4
+ * The eval proves the north-star claim "any client → any workflow": a held-out
5
+ * corpus of GIS workflows is driven through the honua MCP surface by DIFFERENT
6
+ * client LLMs (Claude AND a GPT model), and end-to-end success / clarification /
7
+ * edit rates are recorded per model. A deterministic (scripted) driver provides
8
+ * an offline control that runs in CI with no model/API calls.
9
+ */
10
+ /** A tool as advertised by the MCP surface, handed to a model driver. */
11
+ export interface ToolDescriptor {
12
+ name: string;
13
+ description: string;
14
+ inputSchema: unknown;
15
+ }
16
+ /** Result of a single `tools/call` over the MCP surface. */
17
+ export interface ToolCallResult {
18
+ isError: boolean;
19
+ /** Flattened text content of the tool result (what a model would read back). */
20
+ text: string;
21
+ }
22
+ /** The MCP surface a driver may exercise while solving a workflow. */
23
+ export interface WorkflowContext {
24
+ tools: ToolDescriptor[];
25
+ callTool(name: string, args: Record<string, unknown>): Promise<ToolCallResult>;
26
+ }
27
+ /** One step a driver took: a tool call and whether it errored. */
28
+ export interface TranscriptStep {
29
+ tool: string;
30
+ args: Record<string, unknown>;
31
+ isError: boolean;
32
+ }
33
+ /** What a driver produced for one scenario. */
34
+ export interface WorkflowTranscript {
35
+ scenarioId: string;
36
+ modelId: string;
37
+ steps: TranscriptStep[];
38
+ finalAnswer: string;
39
+ /** The driver asked the user a clarifying question instead of completing. */
40
+ clarificationRequested: boolean;
41
+ /** Tool calls that returned isError (a proxy for self-correction / edits). */
42
+ errorCount: number;
43
+ /** Driver-level failure (timeout, exception, model unavailable). */
44
+ driverError?: string;
45
+ }
46
+ /** Declarative success criteria graded against a transcript. */
47
+ export interface SuccessCriteria {
48
+ /** Tools that must be called at least once (any order). */
49
+ requiredTools: string[];
50
+ /** An ordered subsequence of tool names that must appear in order. */
51
+ expectedToolSequence?: string[];
52
+ /** Tools that must never be called (e.g. write/destructive tools). */
53
+ forbiddenTools?: string[];
54
+ /** Case-insensitive substrings the final answer must contain. */
55
+ answerMustInclude?: string[];
56
+ }
57
+ /** One GIS workflow in the held-out corpus. */
58
+ export interface Scenario {
59
+ id: string;
60
+ title: string;
61
+ category: string;
62
+ /** The natural-language task handed to the client LLM. */
63
+ prompt: string;
64
+ criteria: SuccessCriteria;
65
+ /**
66
+ * Scripted "ideal client" trajectory used by the deterministic offline driver.
67
+ * Live LLM drivers ignore this and plan their own tool calls.
68
+ */
69
+ script: {
70
+ tool: string;
71
+ args: Record<string, unknown>;
72
+ }[];
73
+ }
74
+ /** Per-scenario grade for one model. */
75
+ export type GradeOutcome = "pass" | "fail" | "clarified" | "error";
76
+ export interface ScenarioGrade {
77
+ scenarioId: string;
78
+ modelId: string;
79
+ outcome: GradeOutcome;
80
+ /** Human-readable reasons a grade was not `pass`. */
81
+ violations: string[];
82
+ errorCount: number;
83
+ }
84
+ /** A driver wraps a single client model (or the deterministic control). */
85
+ export interface ModelDriver {
86
+ /** Stable identifier recorded in the artifact, e.g. "claude-opus-4-8". */
87
+ readonly id: string;
88
+ readonly vendor: "anthropic" | "openai" | "bedrock" | "deterministic";
89
+ /** Whether this driver can actually run (key + SDK present). */
90
+ isAvailable(): boolean;
91
+ runWorkflow(scenario: Scenario, ctx: WorkflowContext): Promise<WorkflowTranscript>;
92
+ }
93
+ //# sourceMappingURL=types.d.ts.map