@bryan-thompson/inspector-assessment-cli 1.36.3 → 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.
@@ -5,7 +5,7 @@
5
5
  * from raw MCP transport responses, even when the SDK's Zod validation would
6
6
  * normally strip them.
7
7
  */
8
- import { jest, describe, it, expect, beforeEach, afterEach } from "@jest/globals";
8
+ import { jest, describe, it, expect, beforeEach, afterEach, } from "@jest/globals";
9
9
  import { getToolsWithPreservedHints, } from "../../lib/assessment-runner/tools-with-hints.js";
10
10
  describe("getToolsWithPreservedHints", () => {
11
11
  let mockClient;
@@ -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", () => {
@@ -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.3",
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>",