@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.
- package/README.md +167 -0
- package/certification/geospatial-mcp-schemas/PROVENANCE.md +33 -0
- package/certification/geospatial-mcp-schemas/README.md +90 -0
- package/certification/geospatial-mcp-schemas/common/geoprocessing-error.schema.json +45 -0
- package/certification/geospatial-mcp-schemas/conformance/manifest.schema.json +73 -0
- package/certification/geospatial-mcp-schemas/index.json +311 -0
- package/certification/geospatial-mcp-schemas/resources/app-package.schema.json +20 -0
- package/certification/geospatial-mcp-schemas/resources/app-template.schema.json +15 -0
- package/certification/geospatial-mcp-schemas/resources/artifact.schema.json +69 -0
- package/certification/geospatial-mcp-schemas/resources/deployment.schema.json +27 -0
- package/certification/geospatial-mcp-schemas/resources/map-package.schema.json +21 -0
- package/certification/geospatial-mcp-schemas/resources/map-template.schema.json +15 -0
- package/certification/geospatial-mcp-schemas/resources/provenance.schema.json +50 -0
- package/certification/geospatial-mcp-schemas/resources/published-service.schema.json +21 -0
- package/certification/geospatial-mcp-schemas/resources/result-package.schema.json +64 -0
- package/certification/geospatial-mcp-schemas/resources/style.schema.json +12 -0
- package/certification/geospatial-mcp-schemas/resources/theme.schema.json +12 -0
- package/certification/geospatial-mcp-schemas/resources/workspace.schema.json +63 -0
- package/certification/geospatial-mcp-schemas/tools/apply_style_preset.schema.json +23 -0
- package/certification/geospatial-mcp-schemas/tools/cancel_job.schema.json +15 -0
- package/certification/geospatial-mcp-schemas/tools/clarify_intent.schema.json +69 -0
- package/certification/geospatial-mcp-schemas/tools/compose_mixed_protocol_map.schema.json +21 -0
- package/certification/geospatial-mcp-schemas/tools/create_app_package.schema.json +33 -0
- package/certification/geospatial-mcp-schemas/tools/create_map_package.schema.json +41 -0
- package/certification/geospatial-mcp-schemas/tools/execute_plan.schema.json +64 -0
- package/certification/geospatial-mcp-schemas/tools/geocode_address.schema.json +36 -0
- package/certification/geospatial-mcp-schemas/tools/get_style.schema.json +30 -0
- package/certification/geospatial-mcp-schemas/tools/ground_candidates.schema.json +66 -0
- package/certification/geospatial-mcp-schemas/tools/list_capabilities.schema.json +21 -0
- package/certification/geospatial-mcp-schemas/tools/list_layers.schema.json +14 -0
- package/certification/geospatial-mcp-schemas/tools/plan_analysis.schema.json +19 -0
- package/certification/geospatial-mcp-schemas/tools/preview_app_package.schema.json +15 -0
- package/certification/geospatial-mcp-schemas/tools/preview_map_package.schema.json +17 -0
- package/certification/geospatial-mcp-schemas/tools/preview_package.schema.json +40 -0
- package/certification/geospatial-mcp-schemas/tools/propose_operation.schema.json +28 -0
- package/certification/geospatial-mcp-schemas/tools/publish_result.schema.json +29 -0
- package/certification/geospatial-mcp-schemas/tools/publish_service.schema.json +59 -0
- package/certification/geospatial-mcp-schemas/tools/query_features.schema.json +54 -0
- package/certification/geospatial-mcp-schemas/tools/refine_map_package.schema.json +22 -0
- package/certification/geospatial-mcp-schemas/tools/render_map.schema.json +55 -0
- package/certification/geospatial-mcp-schemas/tools/resolve_entity.schema.json +30 -0
- package/certification/geospatial-mcp-schemas/tools/solve_route.schema.json +39 -0
- package/certification/geospatial-mcp-schemas/tools/validate_package.schema.json +35 -0
- package/certification/geospatial-mcp-schemas/tools/validate_plan.schema.json +61 -0
- package/dist/src/certification/certifier.d.ts +119 -0
- package/dist/src/certification/certifier.d.ts.map +1 -0
- package/dist/src/certification/certifier.js +410 -0
- package/dist/src/certification/cli.d.ts +3 -0
- package/dist/src/certification/cli.d.ts.map +1 -0
- package/dist/src/certification/cli.js +48 -0
- package/dist/src/certification/contracts.d.ts +62 -0
- package/dist/src/certification/contracts.d.ts.map +1 -0
- package/dist/src/certification/contracts.js +174 -0
- package/dist/src/certification/fixture-client.d.ts +4 -0
- package/dist/src/certification/fixture-client.d.ts.map +1 -0
- package/dist/src/certification/fixture-client.js +113 -0
- package/dist/src/certification/fixtures.d.ts +32 -0
- package/dist/src/certification/fixtures.d.ts.map +1 -0
- package/dist/src/certification/fixtures.js +64 -0
- package/dist/src/certification/json-schema.d.ts +49 -0
- package/dist/src/certification/json-schema.d.ts.map +1 -0
- package/dist/src/certification/json-schema.js +164 -0
- package/dist/src/certification/mock-upstream.d.ts +29 -0
- package/dist/src/certification/mock-upstream.d.ts.map +1 -0
- package/dist/src/certification/mock-upstream.js +96 -0
- package/dist/src/certification/operator-catalog.d.ts +82 -0
- package/dist/src/certification/operator-catalog.d.ts.map +1 -0
- package/dist/src/certification/operator-catalog.js +466 -0
- package/dist/src/certification/report.d.ts +4 -0
- package/dist/src/certification/report.d.ts.map +1 -0
- package/dist/src/certification/report.js +90 -0
- package/dist/src/certification/run.d.ts +28 -0
- package/dist/src/certification/run.d.ts.map +1 -0
- package/dist/src/certification/run.js +31 -0
- package/dist/src/certification/schema-index.d.ts +69 -0
- package/dist/src/certification/schema-index.d.ts.map +1 -0
- package/dist/src/certification/schema-index.js +82 -0
- package/dist/src/certification/target.d.ts +40 -0
- package/dist/src/certification/target.d.ts.map +1 -0
- package/dist/src/certification/target.js +167 -0
- package/dist/src/eval/cli.d.ts +3 -0
- package/dist/src/eval/cli.d.ts.map +1 -0
- package/dist/src/eval/cli.js +116 -0
- package/dist/src/eval/corpus.d.ts +27 -0
- package/dist/src/eval/corpus.d.ts.map +1 -0
- package/dist/src/eval/corpus.js +134 -0
- package/dist/src/eval/drivers/anthropic.d.ts +14 -0
- package/dist/src/eval/drivers/anthropic.d.ts.map +1 -0
- package/dist/src/eval/drivers/anthropic.js +120 -0
- package/dist/src/eval/drivers/bedrock.d.ts +54 -0
- package/dist/src/eval/drivers/bedrock.d.ts.map +1 -0
- package/dist/src/eval/drivers/bedrock.js +178 -0
- package/dist/src/eval/drivers/deterministic.d.ts +17 -0
- package/dist/src/eval/drivers/deterministic.d.ts.map +1 -0
- package/dist/src/eval/drivers/deterministic.js +51 -0
- package/dist/src/eval/drivers/index.d.ts +34 -0
- package/dist/src/eval/drivers/index.d.ts.map +1 -0
- package/dist/src/eval/drivers/index.js +74 -0
- package/dist/src/eval/drivers/openai.d.ts +14 -0
- package/dist/src/eval/drivers/openai.d.ts.map +1 -0
- package/dist/src/eval/drivers/openai.js +136 -0
- package/dist/src/eval/grade.d.ts +14 -0
- package/dist/src/eval/grade.d.ts.map +1 -0
- package/dist/src/eval/grade.js +56 -0
- package/dist/src/eval/northstar-corpus.d.ts +3 -0
- package/dist/src/eval/northstar-corpus.d.ts.map +1 -0
- package/dist/src/eval/northstar-corpus.js +194 -0
- package/dist/src/eval/operator-corpus.d.ts +3 -0
- package/dist/src/eval/operator-corpus.d.ts.map +1 -0
- package/dist/src/eval/operator-corpus.js +193 -0
- package/dist/src/eval/report.d.ts +104 -0
- package/dist/src/eval/report.d.ts.map +1 -0
- package/dist/src/eval/report.js +130 -0
- package/dist/src/eval/runner.d.ts +22 -0
- package/dist/src/eval/runner.d.ts.map +1 -0
- package/dist/src/eval/runner.js +116 -0
- package/dist/src/eval/types.d.ts +93 -0
- package/dist/src/eval/types.d.ts.map +1 -0
- package/dist/src/eval/types.js +10 -0
- package/dist/src/helpers.d.ts +11 -0
- package/dist/src/helpers.d.ts.map +1 -1
- package/dist/src/helpers.js +22 -0
- package/dist/src/index.d.ts +7 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +30 -4
- package/dist/src/proxy.d.ts +46 -0
- package/dist/src/proxy.d.ts.map +1 -0
- package/dist/src/proxy.js +124 -0
- package/dist/src/resources/styles.d.ts +29 -0
- package/dist/src/resources/styles.d.ts.map +1 -0
- package/dist/src/resources/styles.js +43 -0
- package/dist/src/styles.d.ts +110 -0
- package/dist/src/styles.d.ts.map +1 -0
- package/dist/src/styles.js +118 -0
- package/dist/src/tools/apply-style-preset.d.ts +27 -0
- package/dist/src/tools/apply-style-preset.d.ts.map +1 -0
- package/dist/src/tools/apply-style-preset.js +29 -0
- package/dist/src/tools/count-features.d.ts.map +1 -1
- package/dist/src/tools/count-features.js +5 -4
- package/dist/src/tools/get-extent.d.ts.map +1 -1
- package/dist/src/tools/get-extent.js +6 -4
- package/dist/src/tools/get-style.d.ts +24 -0
- package/dist/src/tools/get-style.d.ts.map +1 -0
- package/dist/src/tools/get-style.js +28 -0
- package/dist/src/tools/query-features.d.ts.map +1 -1
- package/dist/src/tools/query-features.js +4 -1
- package/package.json +19 -4
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
import { AUTH_ERROR_CODES, checkAuthContract, checkInvalidArgsContract, checkPaginationContract, } from "./contracts.js";
|
|
2
|
+
import { buildInvalidArgsInputs, buildRoundTripInputs, resolveRoundTripEnv } from "./fixtures.js";
|
|
3
|
+
import { checkConformance, checkWellFormed, validateAgainstSchema } from "./json-schema.js";
|
|
4
|
+
import { buildReferenceToolLookup, loadSchemaFile, loadSchemaIndex, } from "./schema-index.js";
|
|
5
|
+
const STANDARD_SOURCE = "geospatial-mcp@54cbd49 (spec/schemas)";
|
|
6
|
+
/** Determine whether a discovered tool is safe to round-trip (read-only). */
|
|
7
|
+
export function isReadOnlyTool(tool) {
|
|
8
|
+
if (tool.annotations && typeof tool.annotations.readOnlyHint === "boolean") {
|
|
9
|
+
return tool.annotations.readOnlyHint;
|
|
10
|
+
}
|
|
11
|
+
const mutating = /(create|update|delete|publish|apply_.*write|execute|edit|deploy|cancel|propose)/i;
|
|
12
|
+
return !mutating.test(tool.name);
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Connect an MCP client to the certified surface and produce a deterministic
|
|
16
|
+
* certification report. The client is already connected (offline mock upstream,
|
|
17
|
+
* a live `/mcp`, or the stdio proxy); certify never owns its lifecycle.
|
|
18
|
+
*/
|
|
19
|
+
export async function certify(options) {
|
|
20
|
+
const { client } = options;
|
|
21
|
+
const roundTripEnv = resolveRoundTripEnv(options.env);
|
|
22
|
+
const roundTripInputs = buildRoundTripInputs(roundTripEnv);
|
|
23
|
+
const index = loadSchemaIndex();
|
|
24
|
+
const referenceLookup = buildReferenceToolLookup(index);
|
|
25
|
+
const serverInfo = client.getServerVersion() ?? { name: "unknown", version: "0.0.0" };
|
|
26
|
+
const { tools: advertisedTools, pages: toolPages } = await listAllTools(client);
|
|
27
|
+
const { resources: advertisedResources, pages: resourcePages } = await listAllResources(client);
|
|
28
|
+
const prompts = await listPromptsSafely(client);
|
|
29
|
+
const tools = [];
|
|
30
|
+
const advertisedNames = new Set();
|
|
31
|
+
for (const tool of advertisedTools) {
|
|
32
|
+
advertisedNames.add(tool.name);
|
|
33
|
+
tools.push(await certifyTool(client, tool, referenceLookup, roundTripInputs));
|
|
34
|
+
}
|
|
35
|
+
const resources = advertisedResources.map((r) => ({
|
|
36
|
+
name: r.name,
|
|
37
|
+
uri: r.uri,
|
|
38
|
+
discovered: true,
|
|
39
|
+
}));
|
|
40
|
+
const contracts = [];
|
|
41
|
+
contracts.push(paginationCheck("tools", toolPages));
|
|
42
|
+
contracts.push(paginationCheck("resources", resourcePages));
|
|
43
|
+
collectOutputSchemaContracts(tools, contracts);
|
|
44
|
+
contracts.push(await runErrorShapeContract(client, advertisedTools));
|
|
45
|
+
await runAuthContracts(options, advertisedTools, advertisedResources, contracts);
|
|
46
|
+
const knownGaps = collectKnownGaps(index, advertisedNames, referenceLookup);
|
|
47
|
+
return assembleReport({
|
|
48
|
+
serverInfo: serverInfo,
|
|
49
|
+
index,
|
|
50
|
+
options,
|
|
51
|
+
tools,
|
|
52
|
+
resources,
|
|
53
|
+
prompts,
|
|
54
|
+
contracts,
|
|
55
|
+
knownGaps,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
async function listAllTools(client) {
|
|
59
|
+
const tools = [];
|
|
60
|
+
const pages = [];
|
|
61
|
+
let cursor;
|
|
62
|
+
do {
|
|
63
|
+
const resp = await client.listTools(cursor ? { cursor } : undefined);
|
|
64
|
+
tools.push(...resp.tools);
|
|
65
|
+
pages.push({ keys: resp.tools.map((t) => t.name), nextCursor: resp.nextCursor });
|
|
66
|
+
cursor = resp.nextCursor;
|
|
67
|
+
} while (cursor && pages.length < 100);
|
|
68
|
+
return { tools, pages };
|
|
69
|
+
}
|
|
70
|
+
async function listAllResources(client) {
|
|
71
|
+
const resources = [];
|
|
72
|
+
const pages = [];
|
|
73
|
+
let cursor;
|
|
74
|
+
do {
|
|
75
|
+
const resp = await client.listResources(cursor ? { cursor } : undefined);
|
|
76
|
+
resources.push(...resp.resources.map((r) => ({ name: r.name, uri: r.uri })));
|
|
77
|
+
pages.push({ keys: resp.resources.map((r) => r.uri), nextCursor: resp.nextCursor });
|
|
78
|
+
cursor = resp.nextCursor;
|
|
79
|
+
} while (cursor && pages.length < 100);
|
|
80
|
+
return { resources, pages };
|
|
81
|
+
}
|
|
82
|
+
async function listPromptsSafely(client) {
|
|
83
|
+
try {
|
|
84
|
+
const response = await client.listPrompts();
|
|
85
|
+
return response.prompts.map((p) => ({ name: p.name, discovered: true }));
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
return [];
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
async function certifyTool(client, tool, referenceLookup, roundTripInputs) {
|
|
92
|
+
const errors = [];
|
|
93
|
+
const notes = [];
|
|
94
|
+
const wellFormed = checkWellFormed(tool.inputSchema);
|
|
95
|
+
if (!wellFormed.wellFormed) {
|
|
96
|
+
errors.push(...wellFormed.errors.map((e) => `inputSchema not well-formed: ${e}`));
|
|
97
|
+
}
|
|
98
|
+
const hasOutputSchema = tool.outputSchema !== undefined && tool.outputSchema !== null;
|
|
99
|
+
if (hasOutputSchema) {
|
|
100
|
+
const outputWellFormed = checkWellFormed(tool.outputSchema);
|
|
101
|
+
if (!outputWellFormed.wellFormed) {
|
|
102
|
+
errors.push(...outputWellFormed.errors.map((e) => `outputSchema not well-formed: ${e}`));
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
notes.push("no structured output schema advertised");
|
|
107
|
+
}
|
|
108
|
+
const standardEntry = referenceLookup.get(tool.name);
|
|
109
|
+
let conformant = null;
|
|
110
|
+
if (standardEntry) {
|
|
111
|
+
const standardSchema = loadSchemaFile(standardEntry.schema);
|
|
112
|
+
if (wellFormed.wellFormed) {
|
|
113
|
+
const result = checkConformance(tool.inputSchema, standardSchema);
|
|
114
|
+
conformant = result.conformant;
|
|
115
|
+
if (!result.conformant) {
|
|
116
|
+
errors.push(...result.violations.map((v) => `conformance: ${v}`));
|
|
117
|
+
}
|
|
118
|
+
notes.push(...result.notes);
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
conformant = false;
|
|
122
|
+
errors.push("conformance: skipped — inputSchema is not well-formed");
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
const readOnly = isReadOnlyTool(tool);
|
|
126
|
+
let roundTrip = "skipped";
|
|
127
|
+
let structuredOutputValidated = null;
|
|
128
|
+
const fixtureInput = roundTripInputs[tool.name];
|
|
129
|
+
if (readOnly && fixtureInput !== undefined) {
|
|
130
|
+
const outcome = await roundTripTool(client, tool, fixtureInput, errors, notes);
|
|
131
|
+
roundTrip = outcome.roundTrip;
|
|
132
|
+
structuredOutputValidated = outcome.structuredOutputValidated;
|
|
133
|
+
}
|
|
134
|
+
else if (!readOnly) {
|
|
135
|
+
notes.push("round-trip skipped: tool is not read-only");
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
notes.push("round-trip skipped: no fixture input");
|
|
139
|
+
}
|
|
140
|
+
return {
|
|
141
|
+
name: tool.name,
|
|
142
|
+
discovered: true,
|
|
143
|
+
schemaValid: wellFormed.wellFormed,
|
|
144
|
+
hasOutputSchema,
|
|
145
|
+
standardName: standardEntry?.standardName ?? null,
|
|
146
|
+
conformant,
|
|
147
|
+
readOnly,
|
|
148
|
+
roundTrip,
|
|
149
|
+
structuredOutputValidated,
|
|
150
|
+
errors,
|
|
151
|
+
notes,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
async function roundTripTool(client, tool, input, errors, notes) {
|
|
155
|
+
try {
|
|
156
|
+
const result = (await client.callTool({ name: tool.name, arguments: input }));
|
|
157
|
+
if (result.isError) {
|
|
158
|
+
errors.push("round-trip: tool returned isError=true");
|
|
159
|
+
return { roundTrip: "failed", structuredOutputValidated: null };
|
|
160
|
+
}
|
|
161
|
+
if (tool.outputSchema && result.structuredContent !== undefined) {
|
|
162
|
+
const validation = validateAgainstSchema(tool.outputSchema, result.structuredContent);
|
|
163
|
+
if (!validation.valid) {
|
|
164
|
+
errors.push(...validation.errors.map((e) => `output-schema: structuredContent ${e}`));
|
|
165
|
+
return { roundTrip: "failed", structuredOutputValidated: false };
|
|
166
|
+
}
|
|
167
|
+
return { roundTrip: "passed", structuredOutputValidated: true };
|
|
168
|
+
}
|
|
169
|
+
if (tool.outputSchema && result.structuredContent === undefined) {
|
|
170
|
+
errors.push("output-schema: outputSchema advertised but no structuredContent returned");
|
|
171
|
+
return { roundTrip: "failed", structuredOutputValidated: false };
|
|
172
|
+
}
|
|
173
|
+
return { roundTrip: "passed", structuredOutputValidated: null };
|
|
174
|
+
}
|
|
175
|
+
catch (err) {
|
|
176
|
+
errors.push(`round-trip: ${err instanceof Error ? err.message : String(err)}`);
|
|
177
|
+
return { roundTrip: "failed", structuredOutputValidated: null };
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
function paginationCheck(target, pages) {
|
|
181
|
+
const paginated = pages.length > 1 || (pages[0]?.nextCursor !== undefined && pages[0]?.nextCursor !== null);
|
|
182
|
+
if (!paginated) {
|
|
183
|
+
return {
|
|
184
|
+
contract: "list-pagination",
|
|
185
|
+
target,
|
|
186
|
+
status: "skipped",
|
|
187
|
+
detail: `${target} list is a single page (no nextCursor advertised)`,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
const outcome = checkPaginationContract(pages);
|
|
191
|
+
return {
|
|
192
|
+
contract: "list-pagination",
|
|
193
|
+
target,
|
|
194
|
+
status: outcome.ok ? "passed" : "failed",
|
|
195
|
+
detail: outcome.ok
|
|
196
|
+
? `${target} list paginated across ${pages.length} pages; nextCursor honored and terminated`
|
|
197
|
+
: outcome.errors.join("; "),
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
function collectOutputSchemaContracts(tools, contracts) {
|
|
201
|
+
for (const tool of tools) {
|
|
202
|
+
if (tool.structuredOutputValidated === null) {
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
contracts.push({
|
|
206
|
+
contract: "output-schema",
|
|
207
|
+
target: tool.name,
|
|
208
|
+
status: tool.structuredOutputValidated ? "passed" : "failed",
|
|
209
|
+
detail: tool.structuredOutputValidated
|
|
210
|
+
? "structuredContent validated against advertised outputSchema"
|
|
211
|
+
: "structuredContent did not validate against advertised outputSchema",
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
async function runErrorShapeContract(client, advertised) {
|
|
216
|
+
const invalidInputs = buildInvalidArgsInputs();
|
|
217
|
+
const advertisedNames = new Set(advertised.map((t) => t.name));
|
|
218
|
+
const target = Object.keys(invalidInputs).find((name) => advertisedNames.has(name));
|
|
219
|
+
if (!target) {
|
|
220
|
+
return {
|
|
221
|
+
contract: "error-shape",
|
|
222
|
+
target: "(none)",
|
|
223
|
+
status: "skipped",
|
|
224
|
+
detail: "no advertised tool has an invalid-args fixture",
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
try {
|
|
228
|
+
const result = (await client.callTool({ name: target, arguments: invalidInputs[target] }));
|
|
229
|
+
const outcome = checkInvalidArgsContract(result);
|
|
230
|
+
return contractFromOutcome("error-shape", target, outcome);
|
|
231
|
+
}
|
|
232
|
+
catch (err) {
|
|
233
|
+
return {
|
|
234
|
+
contract: "error-shape",
|
|
235
|
+
target,
|
|
236
|
+
status: "failed",
|
|
237
|
+
detail: `invalid arguments raised a protocol error instead of a structured tool error: ${err instanceof Error ? err.message : String(err)}`,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
async function runAuthContracts(options, advertised, resources, contracts) {
|
|
242
|
+
if (!options.connectUnauthenticated) {
|
|
243
|
+
contracts.push({
|
|
244
|
+
contract: "auth-unauthenticated",
|
|
245
|
+
target: "tools/call",
|
|
246
|
+
status: "skipped",
|
|
247
|
+
detail: "target does not support an unauthenticated pass",
|
|
248
|
+
});
|
|
249
|
+
contracts.push({
|
|
250
|
+
contract: "auth-unauthenticated",
|
|
251
|
+
target: "resources/read",
|
|
252
|
+
status: "skipped",
|
|
253
|
+
detail: "target does not support an unauthenticated pass",
|
|
254
|
+
});
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
let unauth;
|
|
258
|
+
try {
|
|
259
|
+
unauth = await options.connectUnauthenticated();
|
|
260
|
+
// tools/call without credentials.
|
|
261
|
+
const toolTarget = advertised.find((t) => isReadOnlyTool(t)) ?? advertised[0];
|
|
262
|
+
if (toolTarget) {
|
|
263
|
+
const roundTripInputs = buildRoundTripInputs(resolveRoundTripEnv(options.env));
|
|
264
|
+
const args = roundTripInputs[toolTarget.name] ?? {};
|
|
265
|
+
contracts.push(await authCheckToolsCall(unauth, toolTarget.name, args));
|
|
266
|
+
}
|
|
267
|
+
// resources/read without credentials.
|
|
268
|
+
if (resources[0]) {
|
|
269
|
+
contracts.push(await authCheckResourcesRead(unauth, resources[0].uri));
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
catch (err) {
|
|
273
|
+
contracts.push({
|
|
274
|
+
contract: "auth-unauthenticated",
|
|
275
|
+
target: "tools/call",
|
|
276
|
+
status: "failed",
|
|
277
|
+
detail: `unauthenticated pass could not be established: ${err instanceof Error ? err.message : String(err)}`,
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
finally {
|
|
281
|
+
if (unauth) {
|
|
282
|
+
await unauth.close().catch(() => { });
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
async function authCheckToolsCall(client, name, args) {
|
|
287
|
+
try {
|
|
288
|
+
const result = await client.callTool({ name, arguments: args });
|
|
289
|
+
if (result.isError !== true) {
|
|
290
|
+
return {
|
|
291
|
+
contract: "auth-unauthenticated",
|
|
292
|
+
target: "tools/call",
|
|
293
|
+
status: "failed",
|
|
294
|
+
detail: `unauthenticated tools/call on ${name} returned a non-error result — auth boundary not enforced`,
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
return contractFromOutcome("auth-unauthenticated", "tools/call", checkAuthContract(result));
|
|
298
|
+
}
|
|
299
|
+
catch (err) {
|
|
300
|
+
// A structured McpError carrying an auth code also satisfies the contract.
|
|
301
|
+
return contractFromOutcome("auth-unauthenticated", "tools/call", checkAuthContract(err));
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
async function authCheckResourcesRead(client, uri) {
|
|
305
|
+
try {
|
|
306
|
+
await client.readResource({ uri });
|
|
307
|
+
return {
|
|
308
|
+
contract: "auth-unauthenticated",
|
|
309
|
+
target: "resources/read",
|
|
310
|
+
status: "failed",
|
|
311
|
+
detail: `unauthenticated resources/read on ${uri} succeeded — auth boundary not enforced`,
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
catch (err) {
|
|
315
|
+
return contractFromOutcome("auth-unauthenticated", "resources/read", checkAuthContract(err));
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
function contractFromOutcome(contract, target, outcome) {
|
|
319
|
+
return {
|
|
320
|
+
contract,
|
|
321
|
+
target,
|
|
322
|
+
status: outcome.ok ? "passed" : "failed",
|
|
323
|
+
detail: outcome.ok ? "contract satisfied" : outcome.errors.join("; "),
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
function collectKnownGaps(index, advertisedNames, referenceLookup) {
|
|
327
|
+
const gaps = [];
|
|
328
|
+
// Advertised names that resolve to a standard entry through the SDK-local
|
|
329
|
+
// reference aliases (e.g. honua_dry_run_plan → validate_plan) also count as
|
|
330
|
+
// covering that standard tool.
|
|
331
|
+
const coveredStandardNames = new Set();
|
|
332
|
+
for (const name of advertisedNames) {
|
|
333
|
+
const entry = referenceLookup.get(name);
|
|
334
|
+
if (entry) {
|
|
335
|
+
coveredStandardNames.add(entry.standardName);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
for (const tool of index.tools) {
|
|
339
|
+
if (!coveredStandardNames.has(tool.standardName)) {
|
|
340
|
+
gaps.push({
|
|
341
|
+
kind: "standard-tool",
|
|
342
|
+
name: tool.standardName,
|
|
343
|
+
family: tool.family,
|
|
344
|
+
detail: tool.implementationStatus === "known-gap"
|
|
345
|
+
? "standard family not yet implemented as a discrete tool"
|
|
346
|
+
: `reference tool ${tool.referenceToolName ?? "(unnamed)"} not advertised by the certified surface`,
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
for (const name of advertisedNames) {
|
|
351
|
+
if (!referenceLookup.has(name)) {
|
|
352
|
+
gaps.push({
|
|
353
|
+
kind: "advertised-tool",
|
|
354
|
+
name,
|
|
355
|
+
detail: "advertised tool has no matching geospatial-mcp standard schema (validated for well-formedness only)",
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
return gaps;
|
|
360
|
+
}
|
|
361
|
+
function assembleReport(args) {
|
|
362
|
+
const { serverInfo, index, options, tools, resources, prompts, contracts, knownGaps } = args;
|
|
363
|
+
const toolsSchemaValid = tools.filter((t) => t.schemaValid).length;
|
|
364
|
+
const conformanceChecked = tools.filter((t) => t.conformant !== null);
|
|
365
|
+
const toolsConformant = conformanceChecked.filter((t) => t.conformant === true).length;
|
|
366
|
+
const toolsRoundTripped = tools.filter((t) => t.roundTrip === "passed").length;
|
|
367
|
+
const toolsOutputValidated = tools.filter((t) => t.structuredOutputValidated === true).length;
|
|
368
|
+
const contractsChecked = contracts.filter((c) => c.status !== "skipped").length;
|
|
369
|
+
const contractsPassed = contracts.filter((c) => c.status === "passed").length;
|
|
370
|
+
const contractsFailed = contracts.filter((c) => c.status === "failed").length;
|
|
371
|
+
const toolFailures = tools.reduce((acc, t) => acc + t.errors.length, 0);
|
|
372
|
+
const failures = toolFailures + contractsFailed;
|
|
373
|
+
const pass = failures === 0;
|
|
374
|
+
return {
|
|
375
|
+
schemaVersion: 2,
|
|
376
|
+
generatedAt: new Date().toISOString(),
|
|
377
|
+
server: serverInfo,
|
|
378
|
+
protocol: {
|
|
379
|
+
mcpTransport: options.targetMode === "stdio-proxy" ? "stdio→streamable-http" : "streamable-http",
|
|
380
|
+
honuaTransport: options.honuaTransport ?? null,
|
|
381
|
+
backend: options.backend,
|
|
382
|
+
targetMode: options.targetMode,
|
|
383
|
+
surface: options.surface,
|
|
384
|
+
},
|
|
385
|
+
standard: { source: STANDARD_SOURCE, indexDate: index.date, dialect: index.dialect },
|
|
386
|
+
summary: {
|
|
387
|
+
pass,
|
|
388
|
+
toolsDiscovered: tools.length,
|
|
389
|
+
toolsSchemaValid,
|
|
390
|
+
toolsConformanceChecked: conformanceChecked.length,
|
|
391
|
+
toolsConformant,
|
|
392
|
+
toolsRoundTripped,
|
|
393
|
+
toolsOutputValidated,
|
|
394
|
+
resourcesDiscovered: resources.length,
|
|
395
|
+
promptsDiscovered: prompts.length,
|
|
396
|
+
contractsChecked,
|
|
397
|
+
contractsPassed,
|
|
398
|
+
contractsFailed,
|
|
399
|
+
knownGaps: knownGaps.length,
|
|
400
|
+
failures,
|
|
401
|
+
},
|
|
402
|
+
tools,
|
|
403
|
+
resources,
|
|
404
|
+
prompts,
|
|
405
|
+
contracts,
|
|
406
|
+
knownGaps,
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
// Re-export for artifact/report consumers.
|
|
410
|
+
export { AUTH_ERROR_CODES };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../../../src/certification/cli.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { runCertification, writeArtifacts } from "./run.js";
|
|
5
|
+
function parseArgs(argv) {
|
|
6
|
+
let artifactOnly = false;
|
|
7
|
+
// Default to the package root (two levels up from dist/src/certification/).
|
|
8
|
+
let outDir = fileURLToPath(new URL("../../../", import.meta.url));
|
|
9
|
+
for (let i = 0; i < argv.length; i++) {
|
|
10
|
+
const arg = argv[i];
|
|
11
|
+
if (arg === "--artifact-only") {
|
|
12
|
+
artifactOnly = true;
|
|
13
|
+
}
|
|
14
|
+
else if (arg === "--out-dir") {
|
|
15
|
+
const value = argv[++i];
|
|
16
|
+
if (!value) {
|
|
17
|
+
throw new Error("--out-dir requires a directory argument");
|
|
18
|
+
}
|
|
19
|
+
outDir = resolve(value);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return { artifactOnly, outDir };
|
|
23
|
+
}
|
|
24
|
+
function artifactPaths(outDir) {
|
|
25
|
+
return {
|
|
26
|
+
json: resolve(outDir, "mcp-certification-results.json"),
|
|
27
|
+
markdown: resolve(outDir, "mcp-certification-results.md"),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
async function main() {
|
|
31
|
+
const { artifactOnly, outDir } = parseArgs(process.argv.slice(2));
|
|
32
|
+
const report = await runCertification();
|
|
33
|
+
const paths = artifactPaths(outDir);
|
|
34
|
+
writeArtifacts(report, paths);
|
|
35
|
+
const s = report.summary;
|
|
36
|
+
const status = s.pass ? "PASS" : "FAIL";
|
|
37
|
+
process.stdout.write(`MCP certification ${status} [${report.protocol.targetMode}]: ${s.toolsConformant}/${s.toolsConformanceChecked} conformant, ` +
|
|
38
|
+
`${s.toolsOutputValidated} output-validated, ${s.contractsPassed}/${s.contractsChecked} contracts, ` +
|
|
39
|
+
`${s.knownGaps} known gaps, ${s.failures} failures.\n`);
|
|
40
|
+
process.stdout.write(`Artifacts: ${paths.json}\n ${paths.markdown}\n`);
|
|
41
|
+
if (!artifactOnly && !report.summary.pass) {
|
|
42
|
+
process.exitCode = 1;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
main().catch((err) => {
|
|
46
|
+
process.stderr.write(`Certification error: ${err instanceof Error ? (err.stack ?? err.message) : String(err)}\n`);
|
|
47
|
+
process.exit(1);
|
|
48
|
+
});
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure, deterministic contract-check analyzers for MCP certification.
|
|
3
|
+
*
|
|
4
|
+
* These operate on values already obtained from the certified surface (tool
|
|
5
|
+
* results, thrown errors, paginated list responses) so they are unit-testable
|
|
6
|
+
* without any transport. The certifier does the I/O; these judge the shape.
|
|
7
|
+
*
|
|
8
|
+
* Contracts enforced (§P0.2):
|
|
9
|
+
* - output-schema : a tool's structuredContent validates against its outputSchema.
|
|
10
|
+
* - error-shape : invalid arguments yield an `isError` tool result carrying a
|
|
11
|
+
* structured, actionable code (validated against the vendored
|
|
12
|
+
* geoprocessing-error schema), never a protocol error.
|
|
13
|
+
* - auth : unauthenticated tools/call and resources/read surface a
|
|
14
|
+
* structured `unauthenticated` / `permission_denied` signal.
|
|
15
|
+
* - pagination : list `nextCursor` is honored and terminates.
|
|
16
|
+
*/
|
|
17
|
+
/** Machine codes that satisfy the unauthenticated / permission-denied contract. */
|
|
18
|
+
export declare const AUTH_ERROR_CODES: readonly ["unauthenticated", "permission_denied"];
|
|
19
|
+
/** geoprocessing-error `kind` values that denote an authorization failure. */
|
|
20
|
+
export declare const AUTH_ERROR_KINDS: readonly ["AuthorizationDenied", "Unauthenticated", "PermissionDenied"];
|
|
21
|
+
export interface StructuredErrorEnvelope {
|
|
22
|
+
code?: string;
|
|
23
|
+
error?: unknown;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Normalize a structured error envelope out of any of the places a surface may
|
|
27
|
+
* carry it: a tool result's `structuredContent`, an `McpError`'s `data`, or a
|
|
28
|
+
* bare geoprocessing-error object. Returns undefined when no structure is found.
|
|
29
|
+
*/
|
|
30
|
+
export declare function extractStructuredError(source: unknown): StructuredErrorEnvelope | undefined;
|
|
31
|
+
export interface ContractOutcome {
|
|
32
|
+
ok: boolean;
|
|
33
|
+
errors: string[];
|
|
34
|
+
}
|
|
35
|
+
/** Validate a value against the vendored geoprocessing-error schema. */
|
|
36
|
+
export declare function validateGeoprocessingError(error: unknown): ContractOutcome;
|
|
37
|
+
/**
|
|
38
|
+
* error-shape contract: an invalid-argument tool call must return an `isError`
|
|
39
|
+
* result whose structuredContent carries an actionable code and a
|
|
40
|
+
* geoprocessing-error-shaped `ValidationFailed` envelope.
|
|
41
|
+
*/
|
|
42
|
+
export declare function checkInvalidArgsContract(result: {
|
|
43
|
+
isError?: boolean;
|
|
44
|
+
structuredContent?: unknown;
|
|
45
|
+
}): ContractOutcome;
|
|
46
|
+
/**
|
|
47
|
+
* auth contract: an unauthenticated call must surface a structured
|
|
48
|
+
* `unauthenticated` / `permission_denied` signal — either as a code on the
|
|
49
|
+
* envelope or as an AuthorizationDenied geoprocessing-error kind.
|
|
50
|
+
*/
|
|
51
|
+
export declare function checkAuthContract(source: unknown): ContractOutcome;
|
|
52
|
+
export interface ListPage {
|
|
53
|
+
/** Distinct item identity keys observed on this page (e.g. tool names). */
|
|
54
|
+
keys: string[];
|
|
55
|
+
nextCursor: string | undefined;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* pagination contract: following `nextCursor` yields additional distinct items,
|
|
59
|
+
* never repeats a cursor (terminates), and the final page has no cursor.
|
|
60
|
+
*/
|
|
61
|
+
export declare function checkPaginationContract(pages: ListPage[]): ContractOutcome;
|
|
62
|
+
//# sourceMappingURL=contracts.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"contracts.d.ts","sourceRoot":"","sources":["../../../src/certification/contracts.ts"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;GAeG;AAEH,mFAAmF;AACnF,eAAO,MAAM,gBAAgB,mDAAoD,CAAC;AAElF,8EAA8E;AAC9E,eAAO,MAAM,gBAAgB,yEAA0E,CAAC;AAUxG,MAAM,WAAW,uBAAuB;IACtC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAMD;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,OAAO,GAAG,uBAAuB,GAAG,SAAS,CAgC3F;AAED,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,OAAO,CAAC;IACZ,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAED,wEAAwE;AACxE,wBAAgB,0BAA0B,CAAC,KAAK,EAAE,OAAO,GAAG,eAAe,CAG1E;AAED;;;;GAIG;AACH,wBAAgB,wBAAwB,CAAC,MAAM,EAAE;IAC/C,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,iBAAiB,CAAC,EAAE,OAAO,CAAC;CAC7B,GAAG,eAAe,CA8BlB;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,OAAO,GAAG,eAAe,CAoBlE;AAED,MAAM,WAAW,QAAQ;IACvB,2EAA2E;IAC3E,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,UAAU,EAAE,MAAM,GAAG,SAAS,CAAC;CAChC;AAED;;;GAGG;AACH,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,QAAQ,EAAE,GAAG,eAAe,CA6B1E"}
|