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

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.
@@ -142,6 +142,71 @@ describe("JSONL Event Emission", () => {
142
142
  emitToolDiscovered(tool);
143
143
  expect(emittedEvents[0].annotations).toBeNull();
144
144
  });
145
+ // Issue #155: Test that direct properties are detected
146
+ it("should detect annotations from direct properties (Issue #155)", () => {
147
+ // Simulate a tool with readOnlyHint as a direct property (not in annotations object)
148
+ // This is how some MCP SDKs serialize annotations
149
+ const tool = {
150
+ name: "browse_subreddit",
151
+ description: "Browse a subreddit",
152
+ inputSchema: { type: "object" },
153
+ readOnlyHint: true, // Direct property, not in annotations object
154
+ };
155
+ emitToolDiscovered(tool);
156
+ const annotations = emittedEvents[0].annotations;
157
+ expect(annotations).not.toBeNull();
158
+ expect(annotations?.readOnlyHint).toBe(true);
159
+ });
160
+ it("should detect annotations from metadata object (Issue #155)", () => {
161
+ // Simulate a tool with annotations in metadata object
162
+ const tool = {
163
+ name: "get_data",
164
+ description: "Get data",
165
+ inputSchema: { type: "object" },
166
+ metadata: {
167
+ readOnlyHint: true,
168
+ destructiveHint: false,
169
+ },
170
+ };
171
+ emitToolDiscovered(tool);
172
+ const annotations = emittedEvents[0].annotations;
173
+ expect(annotations).not.toBeNull();
174
+ expect(annotations?.readOnlyHint).toBe(true);
175
+ expect(annotations?.destructiveHint).toBe(false);
176
+ });
177
+ it("should detect annotations from _meta object (Issue #155)", () => {
178
+ // Simulate a tool with annotations in _meta object
179
+ const tool = {
180
+ name: "search_docs",
181
+ description: "Search documents",
182
+ inputSchema: { type: "object" },
183
+ _meta: {
184
+ idempotentHint: true,
185
+ openWorldHint: false,
186
+ },
187
+ };
188
+ emitToolDiscovered(tool);
189
+ const annotations = emittedEvents[0].annotations;
190
+ expect(annotations).not.toBeNull();
191
+ expect(annotations?.idempotentHint).toBe(true);
192
+ expect(annotations?.openWorldHint).toBe(false);
193
+ });
194
+ it("should prioritize tool.annotations over direct properties (Issue #155)", () => {
195
+ // When both exist, tool.annotations should take precedence
196
+ const tool = {
197
+ name: "conflicting_tool",
198
+ description: "Test priority",
199
+ inputSchema: { type: "object" },
200
+ annotations: {
201
+ readOnlyHint: false, // In annotations object
202
+ },
203
+ readOnlyHint: true, // Direct property (should be ignored)
204
+ };
205
+ emitToolDiscovered(tool);
206
+ const annotations = emittedEvents[0].annotations;
207
+ expect(annotations).not.toBeNull();
208
+ expect(annotations?.readOnlyHint).toBe(false); // Should use annotations object value
209
+ });
145
210
  });
146
211
  describe("emitToolsDiscoveryComplete", () => {
147
212
  it("should emit tools_discovery_complete with count", () => {
@@ -60,16 +60,99 @@ export function extractToolParams(schema) {
60
60
  /**
61
61
  * Emit tool_discovered event for each tool found.
62
62
  * Includes annotations if the server provides them.
63
+ *
64
+ * Issue #155: Check multiple locations for annotations:
65
+ * 1. tool.annotations object (MCP 2024-11 spec)
66
+ * 2. Direct properties (tool.readOnlyHint, etc.)
67
+ * 3. tool.metadata object
68
+ * 4. tool._meta object
63
69
  */
64
70
  export function emitToolDiscovered(tool) {
65
71
  const params = extractToolParams(tool.inputSchema);
66
- // Extract annotations, null if not present
67
- const annotations = tool.annotations
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.
77
+ const toolAny = tool;
78
+ // Priority 1: Check tool.annotations object (MCP spec)
79
+ let readOnlyHint;
80
+ let destructiveHint;
81
+ let idempotentHint;
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;
88
+ }
89
+ // Priority 2: Check direct properties on tool object
90
+ // Only use if not already found in annotations
91
+ if (readOnlyHint === undefined && typeof toolAny.readOnlyHint === "boolean") {
92
+ readOnlyHint = toolAny.readOnlyHint;
93
+ }
94
+ if (destructiveHint === undefined &&
95
+ typeof toolAny.destructiveHint === "boolean") {
96
+ destructiveHint = toolAny.destructiveHint;
97
+ }
98
+ if (idempotentHint === undefined &&
99
+ typeof toolAny.idempotentHint === "boolean") {
100
+ idempotentHint = toolAny.idempotentHint;
101
+ }
102
+ if (openWorldHint === undefined &&
103
+ typeof toolAny.openWorldHint === "boolean") {
104
+ openWorldHint = toolAny.openWorldHint;
105
+ }
106
+ // Priority 3: Check tool.metadata object
107
+ const metadata = toolAny.metadata;
108
+ if (metadata) {
109
+ if (readOnlyHint === undefined &&
110
+ typeof metadata.readOnlyHint === "boolean") {
111
+ readOnlyHint = metadata.readOnlyHint;
112
+ }
113
+ if (destructiveHint === undefined &&
114
+ typeof metadata.destructiveHint === "boolean") {
115
+ destructiveHint = metadata.destructiveHint;
116
+ }
117
+ if (idempotentHint === undefined &&
118
+ typeof metadata.idempotentHint === "boolean") {
119
+ idempotentHint = metadata.idempotentHint;
120
+ }
121
+ if (openWorldHint === undefined &&
122
+ typeof metadata.openWorldHint === "boolean") {
123
+ openWorldHint = metadata.openWorldHint;
124
+ }
125
+ }
126
+ // Priority 4: Check tool._meta object
127
+ const _meta = toolAny._meta;
128
+ if (_meta) {
129
+ if (readOnlyHint === undefined && typeof _meta.readOnlyHint === "boolean") {
130
+ readOnlyHint = _meta.readOnlyHint;
131
+ }
132
+ if (destructiveHint === undefined &&
133
+ typeof _meta.destructiveHint === "boolean") {
134
+ destructiveHint = _meta.destructiveHint;
135
+ }
136
+ if (idempotentHint === undefined &&
137
+ typeof _meta.idempotentHint === "boolean") {
138
+ idempotentHint = _meta.idempotentHint;
139
+ }
140
+ if (openWorldHint === undefined &&
141
+ typeof _meta.openWorldHint === "boolean") {
142
+ openWorldHint = _meta.openWorldHint;
143
+ }
144
+ }
145
+ // Build annotations object if any hints were found
146
+ const hasAnnotations = readOnlyHint !== undefined ||
147
+ destructiveHint !== undefined ||
148
+ idempotentHint !== undefined ||
149
+ openWorldHint !== undefined;
150
+ const annotations = hasAnnotations
68
151
  ? {
69
- readOnlyHint: tool.annotations.readOnlyHint,
70
- destructiveHint: tool.annotations.destructiveHint,
71
- idempotentHint: tool.annotations.idempotentHint,
72
- openWorldHint: tool.annotations.openWorldHint,
152
+ readOnlyHint,
153
+ destructiveHint,
154
+ idempotentHint,
155
+ openWorldHint,
73
156
  }
74
157
  : null;
75
158
  emitJSONL({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bryan-thompson/inspector-assessment-cli",
3
- "version": "1.36.1",
3
+ "version": "1.36.2",
4
4
  "description": "CLI for the Enhanced MCP Inspector with assessment capabilities",
5
5
  "license": "MIT",
6
6
  "author": "Bryan Thompson <bryan@triepod.ai>",