@bryan-thompson/inspector-assessment 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.
- package/cli/build/__tests__/assessment-runner/tools-with-hints.test.js +91 -1
- package/cli/build/lib/assessment-runner/tools-with-hints.js +45 -5
- package/cli/package.json +1 -1
- package/client/dist/assets/{OAuthCallback-pydLxj3d.js → OAuthCallback-DJ1av7om.js} +1 -1
- package/client/dist/assets/{OAuthDebugCallback-BLEebYQf.js → OAuthDebugCallback-lRXgX7wV.js} +1 -1
- package/client/dist/assets/{index-CVyqQ7s8.js → index-DEdS99fp.js} +4 -4
- package/client/dist/index.html +1 -1
- package/client/lib/services/assessment/modules/annotations/AnnotationDeceptionDetector.d.ts +22 -0
- package/client/lib/services/assessment/modules/annotations/AnnotationDeceptionDetector.d.ts.map +1 -1
- package/client/lib/services/assessment/modules/annotations/AnnotationDeceptionDetector.js +53 -0
- package/client/package.json +1 -1
- package/package.json +1 -1
- package/server/package.json +1 -1
|
@@ -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;
|
|
@@ -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.
|
|
79
|
-
// 3.
|
|
80
|
-
// 4.
|
|
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/cli/package.json
CHANGED
|
@@ -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-
|
|
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);
|
package/client/dist/assets/{OAuthDebugCallback-BLEebYQf.js → OAuthDebugCallback-lRXgX7wV.js}
RENAMED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { r as reactExports, S as SESSION_KEYS, p as parseOAuthCallbackParams, j as jsxRuntimeExports, g as generateOAuthErrorDescription } from "./index-
|
|
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.
|
|
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.
|
|
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-
|
|
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-
|
|
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
|
}
|
package/client/dist/index.html
CHANGED
|
@@ -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-
|
|
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).
|
package/client/lib/services/assessment/modules/annotations/AnnotationDeceptionDetector.d.ts.map
CHANGED
|
@@ -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,
|
|
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",
|
package/client/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bryan-thompson/inspector-assessment-client",
|
|
3
|
-
"version": "1.36.
|
|
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
|
+
"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>",
|
package/server/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bryan-thompson/inspector-assessment-server",
|
|
3
|
-
"version": "1.36.
|
|
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>",
|