@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. annotations object
79
- // 3. metadata object
80
- // 4. _meta object
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bryan-thompson/inspector-assessment-cli",
3
- "version": "1.36.4",
3
+ "version": "1.37.0",
4
4
  "description": "CLI for the Enhanced MCP Inspector with assessment capabilities",
5
5
  "license": "MIT",
6
6
  "author": "Bryan Thompson <bryan@triepod.ai>",