@bryan-thompson/inspector-assessment 1.36.3 → 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.
@@ -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;
@@ -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;
@@ -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", () => {
@@ -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
  }
@@ -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/cli/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.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>",
@@ -1,4 +1,4 @@
1
- import { u as useToast, r as reactExports, j as jsxRuntimeExports, p as parseOAuthCallbackParams, g as generateOAuthErrorDescription, S as SESSION_KEYS, I as InspectorOAuthClientProvider, a as auth } from "./index-0DAqp50J.js";
1
+ import { u as useToast, r as reactExports, j as jsxRuntimeExports, p as parseOAuthCallbackParams, g as generateOAuthErrorDescription, S as SESSION_KEYS, I as InspectorOAuthClientProvider, a as auth } from "./index-DEdS99fp.js";
2
2
  const OAuthCallback = ({ onConnect }) => {
3
3
  const { toast } = useToast();
4
4
  const hasProcessedRef = reactExports.useRef(false);
@@ -1,4 +1,4 @@
1
- import { r as reactExports, S as SESSION_KEYS, p as parseOAuthCallbackParams, j as jsxRuntimeExports, g as generateOAuthErrorDescription } from "./index-0DAqp50J.js";
1
+ import { r as reactExports, S as SESSION_KEYS, p as parseOAuthCallbackParams, j as jsxRuntimeExports, g as generateOAuthErrorDescription } from "./index-DEdS99fp.js";
2
2
  const OAuthDebugCallback = ({ onConnect }) => {
3
3
  reactExports.useEffect(() => {
4
4
  let isProcessed = false;
@@ -16373,7 +16373,7 @@ object({
16373
16373
  token_type_hint: string().optional()
16374
16374
  }).strip();
16375
16375
  const name = "@bryan-thompson/inspector-assessment-client";
16376
- const version$1 = "1.36.3";
16376
+ const version$1 = "1.36.5";
16377
16377
  const packageJson = {
16378
16378
  name,
16379
16379
  version: version$1
@@ -48920,7 +48920,7 @@ const useTheme = () => {
48920
48920
  [theme, setThemeWithSideEffect]
48921
48921
  );
48922
48922
  };
48923
- const version = "1.36.3";
48923
+ const version = "1.36.5";
48924
48924
  var [createTooltipContext] = createContextScope("Tooltip", [
48925
48925
  createPopperScope
48926
48926
  ]);
@@ -52515,13 +52515,13 @@ const App = () => {
52515
52515
  ) });
52516
52516
  if (window.location.pathname === "/oauth/callback") {
52517
52517
  const OAuthCallback = React.lazy(
52518
- () => __vitePreload(() => import("./OAuthCallback-RXmtS9Xr.js"), true ? [] : void 0)
52518
+ () => __vitePreload(() => import("./OAuthCallback-DJ1av7om.js"), true ? [] : void 0)
52519
52519
  );
52520
52520
  return /* @__PURE__ */ jsxRuntimeExports.jsx(reactExports.Suspense, { fallback: /* @__PURE__ */ jsxRuntimeExports.jsx("div", { children: "Loading..." }), children: /* @__PURE__ */ jsxRuntimeExports.jsx(OAuthCallback, { onConnect: onOAuthConnect }) });
52521
52521
  }
52522
52522
  if (window.location.pathname === "/oauth/callback/debug") {
52523
52523
  const OAuthDebugCallback = React.lazy(
52524
- () => __vitePreload(() => import("./OAuthDebugCallback-BRtDZYF8.js"), true ? [] : void 0)
52524
+ () => __vitePreload(() => import("./OAuthDebugCallback-lRXgX7wV.js"), true ? [] : void 0)
52525
52525
  );
52526
52526
  return /* @__PURE__ */ jsxRuntimeExports.jsx(reactExports.Suspense, { fallback: /* @__PURE__ */ jsxRuntimeExports.jsx("div", { children: "Loading..." }), children: /* @__PURE__ */ jsxRuntimeExports.jsx(OAuthDebugCallback, { onConnect: onOAuthDebugConnect }) });
52527
52527
  }
@@ -5,7 +5,7 @@
5
5
  <link rel="icon" type="image/svg+xml" href="/mcp.svg" />
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
7
  <title>MCP Inspector</title>
8
- <script type="module" crossorigin src="/assets/index-0DAqp50J.js"></script>
8
+ <script type="module" crossorigin src="/assets/index-DEdS99fp.js"></script>
9
9
  <link rel="stylesheet" crossorigin href="/assets/index-BoUA5OL1.css">
10
10
  </head>
11
11
  <body>
@@ -15,6 +15,18 @@ export declare const READONLY_CONTRADICTION_KEYWORDS: string[];
15
15
  * Issue #18: browser-tools-mcp uses runAccessibilityAudit, runSEOAudit, etc.
16
16
  */
17
17
  export declare const RUN_READONLY_EXEMPT_SUFFIXES: string[];
18
+ /**
19
+ * Prefixes that indicate read-only operations.
20
+ * Issue #161: When a tool starts with these prefixes, certain keywords become nouns.
21
+ * For example, "post" in "get_post_details" is a Reddit post (noun), not POST method (verb).
22
+ */
23
+ export declare const READONLY_PREFIX_PATTERNS: RegExp[];
24
+ /**
25
+ * Keywords that can be nouns when following a read-only prefix.
26
+ * Issue #161: These words are verbs when used as prefixes (post_message) but
27
+ * nouns when used after read-only prefixes (get_post_details).
28
+ */
29
+ export declare const NOUN_KEYWORDS_IN_READONLY_CONTEXT: string[];
18
30
  /**
19
31
  * Keywords that contradict destructiveHint=false (these tools delete/destroy data)
20
32
  */
@@ -41,6 +53,16 @@ export declare function containsKeyword(toolName: string, keywords: string[]): s
41
53
  * Issue #18: Prevents false positives for analysis/audit tools.
42
54
  */
43
55
  export declare function isRunKeywordExempt(toolName: string): boolean;
56
+ /**
57
+ * Check if a keyword is being used as a noun in a read-only context.
58
+ * Issue #161: "post" in "get_post_details" is a Reddit post (noun), not POST method (verb).
59
+ * Prevents false positives when read-only tools reference content types.
60
+ *
61
+ * @param toolName - The tool name to check
62
+ * @param keyword - The keyword that was matched (e.g., "post")
63
+ * @returns true if the keyword is a noun in this read-only context
64
+ */
65
+ export declare function isNounInReadOnlyContext(toolName: string, keyword: string): boolean;
44
66
  /**
45
67
  * Type guard for confidence levels that warrant event emission or status changes.
46
68
  * Uses positive check for acceptable levels (safer than !== "low" if new levels added).
@@ -1 +1 @@
1
- {"version":3,"file":"AnnotationDeceptionDetector.d.ts","sourceRoot":"","sources":["../../../../../src/services/assessment/modules/annotations/AnnotationDeceptionDetector.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH;;GAEG;AACH,eAAO,MAAM,+BAA+B,UA0C3C,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,4BAA4B,UAexC,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,kCAAkC,UAgB9C,CAAC;AAEF;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,cAAc,GAAG,iBAAiB,CAAC;IAC1C,cAAc,EAAE,MAAM,CAAC;IACvB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAC7B,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,EAAE,GACjB,MAAM,GAAG,IAAI,CAkBf;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAU5D;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAElE;AAED;;;GAGG;AACH,wBAAgB,yBAAyB,CACvC,QAAQ,EAAE,MAAM,EAChB,WAAW,EAAE;IAAE,YAAY,CAAC,EAAE,OAAO,CAAC;IAAC,eAAe,CAAC,EAAE,OAAO,CAAA;CAAE,GACjE,eAAe,GAAG,IAAI,CAoCxB"}
1
+ {"version":3,"file":"AnnotationDeceptionDetector.d.ts","sourceRoot":"","sources":["../../../../../src/services/assessment/modules/annotations/AnnotationDeceptionDetector.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH;;GAEG;AACH,eAAO,MAAM,+BAA+B,UA0C3C,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,4BAA4B,UAexC,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,wBAAwB,UAYpC,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,iCAAiC,UAO7C,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,kCAAkC,UAgB9C,CAAC;AAEF;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,cAAc,GAAG,iBAAiB,CAAC;IAC1C,cAAc,EAAE,MAAM,CAAC;IACvB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAC7B,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,EAAE,GACjB,MAAM,GAAG,IAAI,CAkBf;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAU5D;AAED;;;;;;;;GAQG;AACH,wBAAgB,uBAAuB,CACrC,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,GACd,OAAO,CAOT;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAElE;AAED;;;GAGG;AACH,wBAAgB,yBAAyB,CACvC,QAAQ,EAAE,MAAM,EAChB,WAAW,EAAE;IAAE,YAAY,CAAC,EAAE,OAAO,CAAC;IAAC,eAAe,CAAC,EAAE,OAAO,CAAA;CAAE,GACjE,eAAe,GAAG,IAAI,CAwCxB"}
@@ -72,6 +72,37 @@ export const RUN_READONLY_EXEMPT_SUFFIXES = [
72
72
  "benchmark", // runBenchmark, runPerfBenchmark
73
73
  "diagnostic", // runDiagnostic
74
74
  ];
75
+ /**
76
+ * Prefixes that indicate read-only operations.
77
+ * Issue #161: When a tool starts with these prefixes, certain keywords become nouns.
78
+ * For example, "post" in "get_post_details" is a Reddit post (noun), not POST method (verb).
79
+ */
80
+ export const READONLY_PREFIX_PATTERNS = [
81
+ /^get[_-]/i,
82
+ /^list[_-]/i,
83
+ /^fetch[_-]/i,
84
+ /^read[_-]/i,
85
+ /^search[_-]/i,
86
+ /^find[_-]/i,
87
+ /^show[_-]/i,
88
+ /^view[_-]/i,
89
+ /^describe[_-]/i,
90
+ /^check[_-]/i,
91
+ /^query[_-]/i,
92
+ ];
93
+ /**
94
+ * Keywords that can be nouns when following a read-only prefix.
95
+ * Issue #161: These words are verbs when used as prefixes (post_message) but
96
+ * nouns when used after read-only prefixes (get_post_details).
97
+ */
98
+ export const NOUN_KEYWORDS_IN_READONLY_CONTEXT = [
99
+ "post", // Reddit post, blog post, forum post
100
+ "message", // chat message, email message
101
+ "comment", // post comment, code comment
102
+ "thread", // forum thread, email thread
103
+ "log", // log entry, audit log
104
+ "record", // database record
105
+ ];
75
106
  /**
76
107
  * Keywords that contradict destructiveHint=false (these tools delete/destroy data)
77
108
  */
@@ -130,6 +161,23 @@ export function isRunKeywordExempt(toolName) {
130
161
  // Check if any exempt suffix is present
131
162
  return RUN_READONLY_EXEMPT_SUFFIXES.some((suffix) => lowerName.includes(suffix));
132
163
  }
164
+ /**
165
+ * Check if a keyword is being used as a noun in a read-only context.
166
+ * Issue #161: "post" in "get_post_details" is a Reddit post (noun), not POST method (verb).
167
+ * Prevents false positives when read-only tools reference content types.
168
+ *
169
+ * @param toolName - The tool name to check
170
+ * @param keyword - The keyword that was matched (e.g., "post")
171
+ * @returns true if the keyword is a noun in this read-only context
172
+ */
173
+ export function isNounInReadOnlyContext(toolName, keyword) {
174
+ // Check if keyword can be a noun in read-only context
175
+ if (!NOUN_KEYWORDS_IN_READONLY_CONTEXT.includes(keyword)) {
176
+ return false;
177
+ }
178
+ // Check if tool name starts with read-only prefix
179
+ return READONLY_PREFIX_PATTERNS.some((pattern) => pattern.test(toolName));
180
+ }
133
181
  /**
134
182
  * Type guard for confidence levels that warrant event emission or status changes.
135
183
  * Uses positive check for acceptable levels (safer than !== "low" if new levels added).
@@ -152,6 +200,11 @@ export function detectAnnotationDeception(toolName, annotations) {
152
200
  // Tool matches "run" but has an analysis suffix - not deceptive
153
201
  // Fall through to normal pattern-based inference
154
202
  }
203
+ else if (isNounInReadOnlyContext(toolName, keyword)) {
204
+ // Issue #161: Skip when keyword is a noun in read-only context
205
+ // e.g., "post" in "get_post_details" is a Reddit post, not POST method
206
+ // Fall through to normal pattern-based inference
207
+ }
155
208
  else {
156
209
  return {
157
210
  field: "readOnlyHint",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bryan-thompson/inspector-assessment-client",
3
- "version": "1.36.3",
3
+ "version": "1.36.5",
4
4
  "description": "Client-side application for the Enhanced MCP Inspector with assessment capabilities",
5
5
  "license": "MIT",
6
6
  "author": "Bryan Thompson <bryan@triepod.ai>",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bryan-thompson/inspector-assessment",
3
- "version": "1.36.3",
3
+ "version": "1.36.5",
4
4
  "description": "Enhanced MCP Inspector with comprehensive assessment capabilities for server validation",
5
5
  "license": "MIT",
6
6
  "author": "Bryan Thompson <bryan@triepod.ai>",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bryan-thompson/inspector-assessment-server",
3
- "version": "1.36.3",
3
+ "version": "1.36.5",
4
4
  "description": "Server-side application for the Enhanced MCP Inspector with assessment capabilities",
5
5
  "license": "MIT",
6
6
  "author": "Bryan Thompson <bryan@triepod.ai>",