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

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;
@@ -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.36.5",
4
4
  "description": "CLI for the Enhanced MCP Inspector with assessment capabilities",
5
5
  "license": "MIT",
6
6
  "author": "Bryan Thompson <bryan@triepod.ai>",