@bryan-thompson/inspector-assessment-cli 1.36.2 → 1.36.4

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.
@@ -0,0 +1,224 @@
1
+ /**
2
+ * Tests for tools-with-hints module
3
+ *
4
+ * Issue #155: Verifies that hint properties (readOnlyHint, etc.) are preserved
5
+ * from raw MCP transport responses, even when the SDK's Zod validation would
6
+ * normally strip them.
7
+ */
8
+ import { jest, describe, it, expect, beforeEach, afterEach, } from "@jest/globals";
9
+ import { getToolsWithPreservedHints, } from "../../lib/assessment-runner/tools-with-hints.js";
10
+ describe("getToolsWithPreservedHints", () => {
11
+ let mockClient;
12
+ let mockTransport;
13
+ beforeEach(() => {
14
+ mockTransport = {
15
+ onmessage: undefined,
16
+ };
17
+ mockClient = {
18
+ transport: mockTransport,
19
+ listTools: jest.fn(),
20
+ };
21
+ });
22
+ afterEach(() => {
23
+ jest.clearAllMocks();
24
+ });
25
+ it("should preserve readOnlyHint from direct property in raw response", async () => {
26
+ // SDK returns tool without readOnlyHint (stripped by Zod)
27
+ mockClient.listTools.mockImplementation(async () => {
28
+ // Simulate raw response before SDK processes it
29
+ if (mockTransport.onmessage) {
30
+ mockTransport.onmessage({
31
+ result: {
32
+ tools: [
33
+ {
34
+ name: "browse_subreddit",
35
+ description: "Browse posts",
36
+ readOnlyHint: true, // Direct property in raw response
37
+ },
38
+ ],
39
+ },
40
+ });
41
+ }
42
+ // Return SDK-processed tools (without direct readOnlyHint)
43
+ return {
44
+ tools: [
45
+ {
46
+ name: "browse_subreddit",
47
+ description: "Browse posts",
48
+ // readOnlyHint stripped by SDK Zod validation
49
+ },
50
+ ],
51
+ };
52
+ });
53
+ const tools = await getToolsWithPreservedHints(mockClient);
54
+ expect(tools).toHaveLength(1);
55
+ expect(tools[0].name).toBe("browse_subreddit");
56
+ expect(tools[0].readOnlyHint).toBe(true);
57
+ });
58
+ it("should preserve all hint properties from raw response", async () => {
59
+ mockClient.listTools.mockImplementation(async () => {
60
+ if (mockTransport.onmessage) {
61
+ mockTransport.onmessage({
62
+ result: {
63
+ tools: [
64
+ {
65
+ name: "delete_item",
66
+ readOnlyHint: false,
67
+ destructiveHint: true,
68
+ idempotentHint: false,
69
+ openWorldHint: true,
70
+ },
71
+ ],
72
+ },
73
+ });
74
+ }
75
+ return { tools: [{ name: "delete_item" }] };
76
+ });
77
+ const tools = await getToolsWithPreservedHints(mockClient);
78
+ expect(tools[0].readOnlyHint).toBe(false);
79
+ expect(tools[0].destructiveHint).toBe(true);
80
+ expect(tools[0].idempotentHint).toBe(false);
81
+ expect(tools[0].openWorldHint).toBe(true);
82
+ });
83
+ it("should not override SDK annotations with raw direct properties", async () => {
84
+ // If SDK has annotations, they take precedence
85
+ mockClient.listTools.mockImplementation(async () => {
86
+ if (mockTransport.onmessage) {
87
+ mockTransport.onmessage({
88
+ result: {
89
+ tools: [
90
+ {
91
+ name: "read_file",
92
+ readOnlyHint: false, // Raw says false
93
+ },
94
+ ],
95
+ },
96
+ });
97
+ }
98
+ return {
99
+ tools: [
100
+ {
101
+ name: "read_file",
102
+ annotations: {
103
+ readOnlyHint: true, // SDK annotations say true
104
+ },
105
+ },
106
+ ],
107
+ };
108
+ });
109
+ const tools = await getToolsWithPreservedHints(mockClient);
110
+ // SDK annotations should take precedence
111
+ expect(tools[0].annotations?.readOnlyHint).toBe(true);
112
+ // Direct property should NOT override
113
+ expect(tools[0].readOnlyHint).toBeUndefined();
114
+ });
115
+ it("should handle tools with no raw match gracefully", async () => {
116
+ mockClient.listTools.mockImplementation(async () => {
117
+ if (mockTransport.onmessage) {
118
+ mockTransport.onmessage({
119
+ result: {
120
+ tools: [
121
+ { name: "tool_a", readOnlyHint: true },
122
+ // tool_b missing from raw
123
+ ],
124
+ },
125
+ });
126
+ }
127
+ return {
128
+ tools: [
129
+ { name: "tool_a" },
130
+ { name: "tool_b" }, // Not in raw response
131
+ ],
132
+ };
133
+ });
134
+ const tools = await getToolsWithPreservedHints(mockClient);
135
+ expect(tools).toHaveLength(2);
136
+ expect(tools[0].readOnlyHint).toBe(true);
137
+ expect(tools[1].readOnlyHint).toBeUndefined();
138
+ });
139
+ it("should fallback to SDK tools when transport not available", async () => {
140
+ mockClient.transport = null;
141
+ mockClient.listTools.mockImplementation(async () => ({
142
+ tools: [{ name: "test_tool", annotations: { readOnlyHint: true } }],
143
+ }));
144
+ const tools = await getToolsWithPreservedHints(mockClient);
145
+ expect(tools).toHaveLength(1);
146
+ expect(tools[0].name).toBe("test_tool");
147
+ });
148
+ it("should fallback to SDK tools when raw capture fails", async () => {
149
+ mockClient.listTools.mockImplementation(async () => {
150
+ // Don't trigger onmessage (simulating capture failure)
151
+ return {
152
+ tools: [{ name: "fallback_tool" }],
153
+ };
154
+ });
155
+ const tools = await getToolsWithPreservedHints(mockClient);
156
+ expect(tools).toHaveLength(1);
157
+ expect(tools[0].name).toBe("fallback_tool");
158
+ });
159
+ it("should preserve hints from metadata object", async () => {
160
+ mockClient.listTools.mockImplementation(async () => {
161
+ if (mockTransport.onmessage) {
162
+ mockTransport.onmessage({
163
+ result: {
164
+ tools: [
165
+ {
166
+ name: "query_db",
167
+ metadata: {
168
+ readOnlyHint: true,
169
+ },
170
+ },
171
+ ],
172
+ },
173
+ });
174
+ }
175
+ return { tools: [{ name: "query_db" }] };
176
+ });
177
+ const tools = await getToolsWithPreservedHints(mockClient);
178
+ expect(tools[0].readOnlyHint).toBe(true);
179
+ });
180
+ it("should preserve hints from _meta object", async () => {
181
+ mockClient.listTools.mockImplementation(async () => {
182
+ if (mockTransport.onmessage) {
183
+ mockTransport.onmessage({
184
+ result: {
185
+ tools: [
186
+ {
187
+ name: "custom_tool",
188
+ _meta: {
189
+ destructiveHint: true,
190
+ },
191
+ },
192
+ ],
193
+ },
194
+ });
195
+ }
196
+ return { tools: [{ name: "custom_tool" }] };
197
+ });
198
+ const tools = await getToolsWithPreservedHints(mockClient);
199
+ expect(tools[0].destructiveHint).toBe(true);
200
+ });
201
+ it("should restore original onmessage handler after call", async () => {
202
+ const originalHandler = jest.fn();
203
+ mockTransport.onmessage = originalHandler;
204
+ mockClient.listTools.mockImplementation(async () => {
205
+ if (mockTransport.onmessage) {
206
+ mockTransport.onmessage({ result: { tools: [] } });
207
+ }
208
+ return { tools: [] };
209
+ });
210
+ await getToolsWithPreservedHints(mockClient);
211
+ // Original handler should be restored
212
+ expect(mockTransport.onmessage).toBe(originalHandler);
213
+ });
214
+ it("should restore handler even on error", async () => {
215
+ const originalHandler = jest.fn();
216
+ mockTransport.onmessage = originalHandler;
217
+ mockClient.listTools.mockImplementation(async () => {
218
+ throw new Error("Test error");
219
+ });
220
+ await expect(getToolsWithPreservedHints(mockClient)).rejects.toThrow("Test error");
221
+ // Original handler should be restored despite error
222
+ expect(mockTransport.onmessage).toBe(originalHandler);
223
+ });
224
+ });
@@ -207,6 +207,105 @@ describe("JSONL Event Emission", () => {
207
207
  expect(annotations).not.toBeNull();
208
208
  expect(annotations?.readOnlyHint).toBe(false); // Should use annotations object value
209
209
  });
210
+ // Issue #160: Test non-suffixed annotation property detection
211
+ it("should detect non-suffixed readOnly from annotations object (Issue #160)", () => {
212
+ const tool = {
213
+ name: "domain_search",
214
+ description: "Search emails based on domain name",
215
+ inputSchema: { type: "object" },
216
+ annotations: { readOnly: true }, // Non-suffixed version
217
+ };
218
+ emitToolDiscovered(tool);
219
+ const annotations = emittedEvents[0].annotations;
220
+ expect(annotations).not.toBeNull();
221
+ expect(annotations?.readOnlyHint).toBe(true);
222
+ });
223
+ it("should detect all non-suffixed annotations from annotations object (Issue #160)", () => {
224
+ const tool = {
225
+ name: "full_annotations",
226
+ inputSchema: { type: "object" },
227
+ annotations: {
228
+ readOnly: true,
229
+ destructive: false,
230
+ idempotent: true,
231
+ openWorld: false,
232
+ },
233
+ };
234
+ emitToolDiscovered(tool);
235
+ const annotations = emittedEvents[0].annotations;
236
+ expect(annotations).not.toBeNull();
237
+ expect(annotations?.readOnlyHint).toBe(true);
238
+ expect(annotations?.destructiveHint).toBe(false);
239
+ expect(annotations?.idempotentHint).toBe(true);
240
+ expect(annotations?.openWorldHint).toBe(false);
241
+ });
242
+ it("should detect non-suffixed readOnly from direct properties (Issue #160)", () => {
243
+ const tool = {
244
+ name: "email_finder",
245
+ inputSchema: { type: "object" },
246
+ readOnly: true, // Direct non-suffixed property
247
+ };
248
+ emitToolDiscovered(tool);
249
+ const annotations = emittedEvents[0].annotations;
250
+ expect(annotations).not.toBeNull();
251
+ expect(annotations?.readOnlyHint).toBe(true);
252
+ });
253
+ it("should detect non-suffixed annotations from metadata object (Issue #160)", () => {
254
+ const tool = {
255
+ name: "metadata_tool",
256
+ inputSchema: { type: "object" },
257
+ metadata: {
258
+ readOnly: true,
259
+ destructive: false,
260
+ },
261
+ };
262
+ emitToolDiscovered(tool);
263
+ const annotations = emittedEvents[0].annotations;
264
+ expect(annotations).not.toBeNull();
265
+ expect(annotations?.readOnlyHint).toBe(true);
266
+ expect(annotations?.destructiveHint).toBe(false);
267
+ });
268
+ it("should detect non-suffixed annotations from _meta object (Issue #160)", () => {
269
+ const tool = {
270
+ name: "meta_tool",
271
+ inputSchema: { type: "object" },
272
+ _meta: {
273
+ idempotent: true,
274
+ openWorld: false,
275
+ },
276
+ };
277
+ emitToolDiscovered(tool);
278
+ const annotations = emittedEvents[0].annotations;
279
+ expect(annotations).not.toBeNull();
280
+ expect(annotations?.idempotentHint).toBe(true);
281
+ expect(annotations?.openWorldHint).toBe(false);
282
+ });
283
+ it("should prioritize *Hint over non-suffixed in annotations object (Issue #160)", () => {
284
+ const tool = {
285
+ name: "priority_tool",
286
+ inputSchema: { type: "object" },
287
+ annotations: {
288
+ readOnlyHint: false, // *Hint version should win
289
+ readOnly: true, // Should be ignored
290
+ },
291
+ };
292
+ emitToolDiscovered(tool);
293
+ const annotations = emittedEvents[0].annotations;
294
+ expect(annotations).not.toBeNull();
295
+ expect(annotations?.readOnlyHint).toBe(false); // *Hint takes priority
296
+ });
297
+ it("should prioritize *Hint over non-suffixed in direct properties (Issue #160)", () => {
298
+ const tool = {
299
+ name: "direct_priority_tool",
300
+ inputSchema: { type: "object" },
301
+ destructiveHint: true, // *Hint version should win
302
+ destructive: false, // Should be ignored
303
+ };
304
+ emitToolDiscovered(tool);
305
+ const annotations = emittedEvents[0].annotations;
306
+ expect(annotations).not.toBeNull();
307
+ expect(annotations?.destructiveHint).toBe(true);
308
+ });
210
309
  });
211
310
  describe("emitToolsDiscoveryComplete", () => {
212
311
  it("should emit tools_discovery_complete with count", () => {
@@ -18,6 +18,8 @@ import { createCallToolWrapper } from "./tool-wrapper.js";
18
18
  import { buildConfig } from "./config-builder.js";
19
19
  // Issue #155: Import annotation debug mode setter
20
20
  import { setAnnotationDebugMode } from "../../../../client/lib/services/assessment/modules/annotations/AlignmentChecker.js";
21
+ // Issue #155: Import helper to preserve hint properties stripped by SDK
22
+ import { getToolsWithPreservedHints } from "./tools-with-hints.js";
21
23
  /**
22
24
  * Run full assessment against an MCP server
23
25
  *
@@ -65,9 +67,12 @@ export async function runFullAssessment(options) {
65
67
  if (!serverInfo && !options.jsonOnly) {
66
68
  console.log("⚠️ Server did not provide serverInfo during initialization");
67
69
  }
68
- const response = await client.listTools();
69
- const tools = response.tools || [];
70
+ // Issue #155: Use helper that preserves hint properties stripped by SDK Zod validation
71
+ // The SDK's listTools() validates against a schema that strips direct properties
72
+ // like readOnlyHint. This helper intercepts the raw transport response to preserve them.
73
+ const tools = await getToolsWithPreservedHints(client);
70
74
  // Emit JSONL tool discovery events for audit-worker parsing
75
+ // Tools now have hint properties preserved from raw response
71
76
  for (const tool of tools) {
72
77
  emitToolDiscovered(tool);
73
78
  }
@@ -283,9 +288,9 @@ export async function runFullAssessment(options) {
283
288
  serverName: options.serverName,
284
289
  tools,
285
290
  callTool: createCallToolWrapper(client),
291
+ // Issue #155: Use helper to preserve hint properties in refreshed tool lists
286
292
  listTools: async () => {
287
- const response = await client.listTools();
288
- return response.tools;
293
+ return getToolsWithPreservedHints(client);
289
294
  },
290
295
  config,
291
296
  sourceCodePath: options.sourceCodePath,
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Tools With Preserved Hints
3
+ *
4
+ * Issue #155: The MCP SDK's listTools() method validates responses against
5
+ * a Zod schema that strips properties not explicitly defined (like readOnlyHint
6
+ * as a direct property on tools). This module intercepts the raw transport
7
+ * response to preserve these properties.
8
+ *
9
+ * @module cli/lib/assessment-runner/tools-with-hints
10
+ */
11
+ // Hint property names we want to preserve
12
+ const HINT_PROPERTIES = [
13
+ "readOnlyHint",
14
+ "destructiveHint",
15
+ "idempotentHint",
16
+ "openWorldHint",
17
+ ];
18
+ /**
19
+ * Get tools from MCP server with hint properties preserved.
20
+ *
21
+ * The MCP SDK's listTools() validates against a Zod schema that may strip
22
+ * direct hint properties (readOnlyHint, etc.) that aren't in the schema.
23
+ * This function intercepts the raw transport response to preserve them.
24
+ *
25
+ * @param client - Connected MCP client
26
+ * @returns Tools with hint properties preserved from raw response
27
+ */
28
+ export async function getToolsWithPreservedHints(client) {
29
+ let rawTools = [];
30
+ // Get the underlying transport
31
+ const transport = client.transport;
32
+ if (!transport) {
33
+ // Fallback: just use SDK listTools if transport not accessible
34
+ const response = await client.listTools();
35
+ return response.tools || [];
36
+ }
37
+ // Store original message handler
38
+ const originalOnMessage = transport.onmessage;
39
+ // Intercept messages to capture raw tools/list response
40
+ transport.onmessage = (message, extra) => {
41
+ // Check if this is a tools/list response
42
+ const msg = message;
43
+ if (msg?.result?.tools && Array.isArray(msg.result.tools)) {
44
+ rawTools = msg.result.tools;
45
+ }
46
+ // Call original handler (cast to any to preserve original typing)
47
+ if (originalOnMessage) {
48
+ originalOnMessage(message, extra);
49
+ }
50
+ };
51
+ try {
52
+ // Make SDK call (triggers our interceptor)
53
+ const sdkResponse = await client.listTools();
54
+ const sdkTools = sdkResponse.tools || [];
55
+ // Restore original handler
56
+ transport.onmessage = originalOnMessage;
57
+ // If we didn't capture raw tools, just return SDK tools
58
+ if (rawTools.length === 0) {
59
+ return sdkTools;
60
+ }
61
+ // Merge preserved hint properties from raw into SDK tools
62
+ return sdkTools.map((sdkTool) => {
63
+ // Find matching raw tool by name
64
+ const rawTool = rawTools.find((rt) => rt.name === sdkTool.name);
65
+ if (!rawTool) {
66
+ return sdkTool;
67
+ }
68
+ // Start with SDK tool
69
+ const enrichedTool = { ...sdkTool };
70
+ // Preserve hint properties from raw response (priority order)
71
+ for (const hint of HINT_PROPERTIES) {
72
+ // Skip if SDK already has it via annotations
73
+ if (sdkTool.annotations?.[hint] !== undefined) {
74
+ continue;
75
+ }
76
+ // 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
81
+ let value;
82
+ if (typeof rawTool[hint] === "boolean") {
83
+ value = rawTool[hint];
84
+ }
85
+ else if (typeof rawTool.annotations?.[hint] === "boolean") {
86
+ value = rawTool.annotations[hint];
87
+ }
88
+ else if (typeof rawTool.metadata?.[hint] === "boolean") {
89
+ value = rawTool.metadata[hint];
90
+ }
91
+ else if (typeof rawTool._meta?.[hint] === "boolean") {
92
+ value = rawTool._meta[hint];
93
+ }
94
+ if (value !== undefined) {
95
+ enrichedTool[hint] = value;
96
+ }
97
+ }
98
+ return enrichedTool;
99
+ });
100
+ }
101
+ catch (error) {
102
+ // Restore original handler on error
103
+ transport.onmessage = originalOnMessage;
104
+ throw error;
105
+ }
106
+ }
@@ -70,76 +70,150 @@ export function extractToolParams(schema) {
70
70
  export function emitToolDiscovered(tool) {
71
71
  const params = extractToolParams(tool.inputSchema);
72
72
  // Issue #155: Extract annotations from multiple sources (priority order)
73
- // NOTE: This is a simplified version of AlignmentChecker.extractAnnotations()
74
- // that only checks *Hint-suffixed properties (readOnlyHint, destructiveHint, etc.),
75
- // not non-suffixed variants like readOnly, destructive, idempotent, openWorld.
76
- // See AlignmentChecker.resolveAnnotationValue() for full implementation with fallbacks.
73
+ // Issue #160: Also check non-suffixed variants (readOnly, destructive, etc.)
74
+ // for servers that use the shorter property names instead of *Hint versions.
77
75
  const toolAny = tool;
76
+ const annotationsAny = tool.annotations;
78
77
  // Priority 1: Check tool.annotations object (MCP spec)
78
+ // Issue #160: Use nullish coalescing to fall back to non-suffixed versions
79
79
  let readOnlyHint;
80
80
  let destructiveHint;
81
81
  let idempotentHint;
82
82
  let openWorldHint;
83
- if (tool.annotations) {
84
- readOnlyHint = tool.annotations.readOnlyHint;
85
- destructiveHint = tool.annotations.destructiveHint;
86
- idempotentHint = tool.annotations.idempotentHint;
87
- openWorldHint = tool.annotations.openWorldHint;
83
+ if (annotationsAny) {
84
+ // Check *Hint version first, fall back to non-suffixed (Issue #160)
85
+ if (typeof annotationsAny.readOnlyHint === "boolean") {
86
+ readOnlyHint = annotationsAny.readOnlyHint;
87
+ }
88
+ else if (typeof annotationsAny.readOnly === "boolean") {
89
+ readOnlyHint = annotationsAny.readOnly;
90
+ }
91
+ if (typeof annotationsAny.destructiveHint === "boolean") {
92
+ destructiveHint = annotationsAny.destructiveHint;
93
+ }
94
+ else if (typeof annotationsAny.destructive === "boolean") {
95
+ destructiveHint = annotationsAny.destructive;
96
+ }
97
+ if (typeof annotationsAny.idempotentHint === "boolean") {
98
+ idempotentHint = annotationsAny.idempotentHint;
99
+ }
100
+ else if (typeof annotationsAny.idempotent === "boolean") {
101
+ idempotentHint = annotationsAny.idempotent;
102
+ }
103
+ if (typeof annotationsAny.openWorldHint === "boolean") {
104
+ openWorldHint = annotationsAny.openWorldHint;
105
+ }
106
+ else if (typeof annotationsAny.openWorld === "boolean") {
107
+ openWorldHint = annotationsAny.openWorld;
108
+ }
88
109
  }
89
110
  // Priority 2: Check direct properties on tool object
90
111
  // Only use if not already found in annotations
91
- if (readOnlyHint === undefined && typeof toolAny.readOnlyHint === "boolean") {
92
- readOnlyHint = toolAny.readOnlyHint;
112
+ // Issue #160: Check both *Hint and non-suffixed versions
113
+ if (readOnlyHint === undefined) {
114
+ if (typeof toolAny.readOnlyHint === "boolean") {
115
+ readOnlyHint = toolAny.readOnlyHint;
116
+ }
117
+ else if (typeof toolAny.readOnly === "boolean") {
118
+ readOnlyHint = toolAny.readOnly;
119
+ }
93
120
  }
94
- if (destructiveHint === undefined &&
95
- typeof toolAny.destructiveHint === "boolean") {
96
- destructiveHint = toolAny.destructiveHint;
121
+ if (destructiveHint === undefined) {
122
+ if (typeof toolAny.destructiveHint === "boolean") {
123
+ destructiveHint = toolAny.destructiveHint;
124
+ }
125
+ else if (typeof toolAny.destructive === "boolean") {
126
+ destructiveHint = toolAny.destructive;
127
+ }
97
128
  }
98
- if (idempotentHint === undefined &&
99
- typeof toolAny.idempotentHint === "boolean") {
100
- idempotentHint = toolAny.idempotentHint;
129
+ if (idempotentHint === undefined) {
130
+ if (typeof toolAny.idempotentHint === "boolean") {
131
+ idempotentHint = toolAny.idempotentHint;
132
+ }
133
+ else if (typeof toolAny.idempotent === "boolean") {
134
+ idempotentHint = toolAny.idempotent;
135
+ }
101
136
  }
102
- if (openWorldHint === undefined &&
103
- typeof toolAny.openWorldHint === "boolean") {
104
- openWorldHint = toolAny.openWorldHint;
137
+ if (openWorldHint === undefined) {
138
+ if (typeof toolAny.openWorldHint === "boolean") {
139
+ openWorldHint = toolAny.openWorldHint;
140
+ }
141
+ else if (typeof toolAny.openWorld === "boolean") {
142
+ openWorldHint = toolAny.openWorld;
143
+ }
105
144
  }
106
145
  // Priority 3: Check tool.metadata object
146
+ // Issue #160: Check both *Hint and non-suffixed versions
107
147
  const metadata = toolAny.metadata;
108
148
  if (metadata) {
109
- if (readOnlyHint === undefined &&
110
- typeof metadata.readOnlyHint === "boolean") {
111
- readOnlyHint = metadata.readOnlyHint;
149
+ if (readOnlyHint === undefined) {
150
+ if (typeof metadata.readOnlyHint === "boolean") {
151
+ readOnlyHint = metadata.readOnlyHint;
152
+ }
153
+ else if (typeof metadata.readOnly === "boolean") {
154
+ readOnlyHint = metadata.readOnly;
155
+ }
112
156
  }
113
- if (destructiveHint === undefined &&
114
- typeof metadata.destructiveHint === "boolean") {
115
- destructiveHint = metadata.destructiveHint;
157
+ if (destructiveHint === undefined) {
158
+ if (typeof metadata.destructiveHint === "boolean") {
159
+ destructiveHint = metadata.destructiveHint;
160
+ }
161
+ else if (typeof metadata.destructive === "boolean") {
162
+ destructiveHint = metadata.destructive;
163
+ }
116
164
  }
117
- if (idempotentHint === undefined &&
118
- typeof metadata.idempotentHint === "boolean") {
119
- idempotentHint = metadata.idempotentHint;
165
+ if (idempotentHint === undefined) {
166
+ if (typeof metadata.idempotentHint === "boolean") {
167
+ idempotentHint = metadata.idempotentHint;
168
+ }
169
+ else if (typeof metadata.idempotent === "boolean") {
170
+ idempotentHint = metadata.idempotent;
171
+ }
120
172
  }
121
- if (openWorldHint === undefined &&
122
- typeof metadata.openWorldHint === "boolean") {
123
- openWorldHint = metadata.openWorldHint;
173
+ if (openWorldHint === undefined) {
174
+ if (typeof metadata.openWorldHint === "boolean") {
175
+ openWorldHint = metadata.openWorldHint;
176
+ }
177
+ else if (typeof metadata.openWorld === "boolean") {
178
+ openWorldHint = metadata.openWorld;
179
+ }
124
180
  }
125
181
  }
126
182
  // Priority 4: Check tool._meta object
183
+ // Issue #160: Check both *Hint and non-suffixed versions
127
184
  const _meta = toolAny._meta;
128
185
  if (_meta) {
129
- if (readOnlyHint === undefined && typeof _meta.readOnlyHint === "boolean") {
130
- readOnlyHint = _meta.readOnlyHint;
186
+ if (readOnlyHint === undefined) {
187
+ if (typeof _meta.readOnlyHint === "boolean") {
188
+ readOnlyHint = _meta.readOnlyHint;
189
+ }
190
+ else if (typeof _meta.readOnly === "boolean") {
191
+ readOnlyHint = _meta.readOnly;
192
+ }
131
193
  }
132
- if (destructiveHint === undefined &&
133
- typeof _meta.destructiveHint === "boolean") {
134
- destructiveHint = _meta.destructiveHint;
194
+ if (destructiveHint === undefined) {
195
+ if (typeof _meta.destructiveHint === "boolean") {
196
+ destructiveHint = _meta.destructiveHint;
197
+ }
198
+ else if (typeof _meta.destructive === "boolean") {
199
+ destructiveHint = _meta.destructive;
200
+ }
135
201
  }
136
- if (idempotentHint === undefined &&
137
- typeof _meta.idempotentHint === "boolean") {
138
- idempotentHint = _meta.idempotentHint;
202
+ if (idempotentHint === undefined) {
203
+ if (typeof _meta.idempotentHint === "boolean") {
204
+ idempotentHint = _meta.idempotentHint;
205
+ }
206
+ else if (typeof _meta.idempotent === "boolean") {
207
+ idempotentHint = _meta.idempotent;
208
+ }
139
209
  }
140
- if (openWorldHint === undefined &&
141
- typeof _meta.openWorldHint === "boolean") {
142
- openWorldHint = _meta.openWorldHint;
210
+ if (openWorldHint === undefined) {
211
+ if (typeof _meta.openWorldHint === "boolean") {
212
+ openWorldHint = _meta.openWorldHint;
213
+ }
214
+ else if (typeof _meta.openWorld === "boolean") {
215
+ openWorldHint = _meta.openWorld;
216
+ }
143
217
  }
144
218
  }
145
219
  // Build annotations object if any hints were found
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bryan-thompson/inspector-assessment-cli",
3
- "version": "1.36.2",
3
+ "version": "1.36.4",
4
4
  "description": "CLI for the Enhanced MCP Inspector with assessment capabilities",
5
5
  "license": "MIT",
6
6
  "author": "Bryan Thompson <bryan@triepod.ai>",