@bryan-thompson/inspector-assessment-cli 1.36.4 ā 1.37.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.
|
@@ -198,6 +198,96 @@ describe("getToolsWithPreservedHints", () => {
|
|
|
198
198
|
const tools = await getToolsWithPreservedHints(mockClient);
|
|
199
199
|
expect(tools[0].destructiveHint).toBe(true);
|
|
200
200
|
});
|
|
201
|
+
// Issue #160: Non-suffixed property preservation tests
|
|
202
|
+
it("should preserve non-suffixed readOnly from annotations object (Issue #160)", async () => {
|
|
203
|
+
mockClient.listTools.mockImplementation(async () => {
|
|
204
|
+
if (mockTransport.onmessage) {
|
|
205
|
+
mockTransport.onmessage({
|
|
206
|
+
result: {
|
|
207
|
+
tools: [
|
|
208
|
+
{
|
|
209
|
+
name: "domain_search",
|
|
210
|
+
annotations: { readOnly: true }, // Non-suffixed version
|
|
211
|
+
},
|
|
212
|
+
],
|
|
213
|
+
},
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
// SDK returns tool without annotations (stripped by Zod)
|
|
217
|
+
return { tools: [{ name: "domain_search" }] };
|
|
218
|
+
});
|
|
219
|
+
const tools = await getToolsWithPreservedHints(mockClient);
|
|
220
|
+
// Should be mapped to readOnlyHint
|
|
221
|
+
expect(tools[0].readOnlyHint).toBe(true);
|
|
222
|
+
});
|
|
223
|
+
it("should preserve all non-suffixed annotations from annotations object (Issue #160)", async () => {
|
|
224
|
+
mockClient.listTools.mockImplementation(async () => {
|
|
225
|
+
if (mockTransport.onmessage) {
|
|
226
|
+
mockTransport.onmessage({
|
|
227
|
+
result: {
|
|
228
|
+
tools: [
|
|
229
|
+
{
|
|
230
|
+
name: "full_tool",
|
|
231
|
+
annotations: {
|
|
232
|
+
readOnly: true,
|
|
233
|
+
destructive: false,
|
|
234
|
+
idempotent: true,
|
|
235
|
+
openWorld: false,
|
|
236
|
+
},
|
|
237
|
+
},
|
|
238
|
+
],
|
|
239
|
+
},
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
return { tools: [{ name: "full_tool" }] };
|
|
243
|
+
});
|
|
244
|
+
const tools = await getToolsWithPreservedHints(mockClient);
|
|
245
|
+
expect(tools[0].readOnlyHint).toBe(true);
|
|
246
|
+
expect(tools[0].destructiveHint).toBe(false);
|
|
247
|
+
expect(tools[0].idempotentHint).toBe(true);
|
|
248
|
+
expect(tools[0].openWorldHint).toBe(false);
|
|
249
|
+
});
|
|
250
|
+
it("should preserve non-suffixed readOnly from direct property (Issue #160)", async () => {
|
|
251
|
+
mockClient.listTools.mockImplementation(async () => {
|
|
252
|
+
if (mockTransport.onmessage) {
|
|
253
|
+
mockTransport.onmessage({
|
|
254
|
+
result: {
|
|
255
|
+
tools: [
|
|
256
|
+
{
|
|
257
|
+
name: "direct_tool",
|
|
258
|
+
readOnly: true, // Direct non-suffixed property
|
|
259
|
+
},
|
|
260
|
+
],
|
|
261
|
+
},
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
return { tools: [{ name: "direct_tool" }] };
|
|
265
|
+
});
|
|
266
|
+
const tools = await getToolsWithPreservedHints(mockClient);
|
|
267
|
+
expect(tools[0].readOnlyHint).toBe(true);
|
|
268
|
+
});
|
|
269
|
+
it("should prioritize *Hint over non-suffixed versions (Issue #160)", async () => {
|
|
270
|
+
mockClient.listTools.mockImplementation(async () => {
|
|
271
|
+
if (mockTransport.onmessage) {
|
|
272
|
+
mockTransport.onmessage({
|
|
273
|
+
result: {
|
|
274
|
+
tools: [
|
|
275
|
+
{
|
|
276
|
+
name: "priority_tool",
|
|
277
|
+
annotations: {
|
|
278
|
+
readOnlyHint: false, // *Hint should win
|
|
279
|
+
readOnly: true, // Should be ignored
|
|
280
|
+
},
|
|
281
|
+
},
|
|
282
|
+
],
|
|
283
|
+
},
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
return { tools: [{ name: "priority_tool" }] };
|
|
287
|
+
});
|
|
288
|
+
const tools = await getToolsWithPreservedHints(mockClient);
|
|
289
|
+
expect(tools[0].readOnlyHint).toBe(false); // *Hint takes priority
|
|
290
|
+
});
|
|
201
291
|
it("should restore original onmessage handler after call", async () => {
|
|
202
292
|
const originalHandler = jest.fn();
|
|
203
293
|
mockTransport.onmessage = originalHandler;
|
|
@@ -20,6 +20,8 @@ import { buildConfig } from "./config-builder.js";
|
|
|
20
20
|
import { setAnnotationDebugMode } from "../../../../client/lib/services/assessment/modules/annotations/AlignmentChecker.js";
|
|
21
21
|
// Issue #155: Import helper to preserve hint properties stripped by SDK
|
|
22
22
|
import { getToolsWithPreservedHints } from "./tools-with-hints.js";
|
|
23
|
+
// Issue #168: Import external API dependency detector
|
|
24
|
+
import { ExternalAPIDependencyDetector } from "../../../../client/lib/services/assessment/helpers/ExternalAPIDependencyDetector.js";
|
|
23
25
|
/**
|
|
24
26
|
* Run full assessment against an MCP server
|
|
25
27
|
*
|
|
@@ -284,6 +286,14 @@ export async function runFullAssessment(options) {
|
|
|
284
286
|
// module_started and module_complete are handled by orchestrator directly
|
|
285
287
|
// phase_started and phase_complete are emitted directly (not via callback)
|
|
286
288
|
};
|
|
289
|
+
// Issue #168: Detect external API dependencies before assessors run
|
|
290
|
+
// This enables TemporalAssessor, FunctionalityAssessor, and ErrorHandlingAssessor
|
|
291
|
+
// to adjust their behavior for tools that depend on external APIs
|
|
292
|
+
const apiDetector = new ExternalAPIDependencyDetector();
|
|
293
|
+
const externalAPIDependencies = apiDetector.detect(tools);
|
|
294
|
+
if (!options.jsonOnly && externalAPIDependencies.detectedCount > 0) {
|
|
295
|
+
console.log(`š Detected ${externalAPIDependencies.detectedCount} tool(s) with external API dependencies`);
|
|
296
|
+
}
|
|
287
297
|
const context = {
|
|
288
298
|
serverName: options.serverName,
|
|
289
299
|
tools,
|
|
@@ -305,6 +315,8 @@ export async function runFullAssessment(options) {
|
|
|
305
315
|
// Server info for protocol conformance checks
|
|
306
316
|
serverInfo,
|
|
307
317
|
serverCapabilities: serverCapabilities,
|
|
318
|
+
// Issue #168: External API dependency detection for assessor behavior adjustment
|
|
319
|
+
externalAPIDependencies,
|
|
308
320
|
};
|
|
309
321
|
if (!options.jsonOnly) {
|
|
310
322
|
console.log(`\nš Running assessment with ${Object.keys(config.assessmentCategories || {}).length} modules...`);
|
|
@@ -6,15 +6,33 @@
|
|
|
6
6
|
* as a direct property on tools). This module intercepts the raw transport
|
|
7
7
|
* response to preserve these properties.
|
|
8
8
|
*
|
|
9
|
+
* Issue #160: Also preserves non-suffixed annotation property names (readOnly,
|
|
10
|
+
* destructive, idempotent, openWorld) for servers that use the shorter names
|
|
11
|
+
* instead of the *Hint suffix versions required by MCP spec.
|
|
12
|
+
*
|
|
9
13
|
* @module cli/lib/assessment-runner/tools-with-hints
|
|
10
14
|
*/
|
|
11
|
-
// Hint property names we want to preserve
|
|
15
|
+
// Hint property names we want to preserve (*Hint suffix - MCP spec)
|
|
12
16
|
const HINT_PROPERTIES = [
|
|
13
17
|
"readOnlyHint",
|
|
14
18
|
"destructiveHint",
|
|
15
19
|
"idempotentHint",
|
|
16
20
|
"openWorldHint",
|
|
17
21
|
];
|
|
22
|
+
// Issue #160: Non-suffixed property names (fallback for servers using shorter names)
|
|
23
|
+
const NON_SUFFIXED_PROPERTIES = [
|
|
24
|
+
"readOnly",
|
|
25
|
+
"destructive",
|
|
26
|
+
"idempotent",
|
|
27
|
+
"openWorld",
|
|
28
|
+
];
|
|
29
|
+
// Mapping from non-suffixed to *Hint versions
|
|
30
|
+
const NON_SUFFIXED_TO_HINT = {
|
|
31
|
+
readOnly: "readOnlyHint",
|
|
32
|
+
destructive: "destructiveHint",
|
|
33
|
+
idempotent: "idempotentHint",
|
|
34
|
+
openWorld: "openWorldHint",
|
|
35
|
+
};
|
|
18
36
|
/**
|
|
19
37
|
* Get tools from MCP server with hint properties preserved.
|
|
20
38
|
*
|
|
@@ -74,23 +92,45 @@ export async function getToolsWithPreservedHints(client) {
|
|
|
74
92
|
continue;
|
|
75
93
|
}
|
|
76
94
|
// Check raw tool locations in priority order:
|
|
77
|
-
// 1. Direct property on raw tool
|
|
78
|
-
// 2.
|
|
79
|
-
// 3.
|
|
80
|
-
// 4.
|
|
95
|
+
// 1. Direct property on raw tool (*Hint)
|
|
96
|
+
// 2. Direct property on raw tool (non-suffixed, Issue #160)
|
|
97
|
+
// 3. annotations object (*Hint)
|
|
98
|
+
// 4. annotations object (non-suffixed, Issue #160)
|
|
99
|
+
// 5. metadata object (*Hint)
|
|
100
|
+
// 6. metadata object (non-suffixed, Issue #160)
|
|
101
|
+
// 7. _meta object (*Hint)
|
|
102
|
+
// 8. _meta object (non-suffixed, Issue #160)
|
|
81
103
|
let value;
|
|
104
|
+
// Get the non-suffixed equivalent (e.g., readOnlyHint -> readOnly)
|
|
105
|
+
const nonSuffixed = hint.replace("Hint", "");
|
|
106
|
+
// Check direct properties
|
|
82
107
|
if (typeof rawTool[hint] === "boolean") {
|
|
83
108
|
value = rawTool[hint];
|
|
84
109
|
}
|
|
110
|
+
else if (typeof rawTool[nonSuffixed] === "boolean") {
|
|
111
|
+
value = rawTool[nonSuffixed];
|
|
112
|
+
}
|
|
113
|
+
// Check annotations object
|
|
85
114
|
else if (typeof rawTool.annotations?.[hint] === "boolean") {
|
|
86
115
|
value = rawTool.annotations[hint];
|
|
87
116
|
}
|
|
117
|
+
else if (typeof rawTool.annotations?.[nonSuffixed] === "boolean") {
|
|
118
|
+
value = rawTool.annotations[nonSuffixed];
|
|
119
|
+
}
|
|
120
|
+
// Check metadata object
|
|
88
121
|
else if (typeof rawTool.metadata?.[hint] === "boolean") {
|
|
89
122
|
value = rawTool.metadata[hint];
|
|
90
123
|
}
|
|
124
|
+
else if (typeof rawTool.metadata?.[nonSuffixed] === "boolean") {
|
|
125
|
+
value = rawTool.metadata[nonSuffixed];
|
|
126
|
+
}
|
|
127
|
+
// Check _meta object
|
|
91
128
|
else if (typeof rawTool._meta?.[hint] === "boolean") {
|
|
92
129
|
value = rawTool._meta[hint];
|
|
93
130
|
}
|
|
131
|
+
else if (typeof rawTool._meta?.[nonSuffixed] === "boolean") {
|
|
132
|
+
value = rawTool._meta[nonSuffixed];
|
|
133
|
+
}
|
|
94
134
|
if (value !== undefined) {
|
|
95
135
|
enrichedTool[hint] = value;
|
|
96
136
|
}
|
package/package.json
CHANGED