@bryan-thompson/inspector-assessment-cli 1.36.1 → 1.36.3
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/build/__tests__/assessment-runner/tools-with-hints.test.js +224 -0
- package/build/__tests__/jsonl-events.test.js +65 -0
- package/build/lib/assessment-runner/assessment-executor.js +9 -4
- package/build/lib/assessment-runner/tools-with-hints.js +106 -0
- package/build/lib/jsonl-events.js +89 -6
- package/package.json +1 -1
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for tools-with-hints module
|
|
3
|
+
*
|
|
4
|
+
* Issue #155: Verifies that hint properties (readOnlyHint, etc.) are preserved
|
|
5
|
+
* from raw MCP transport responses, even when the SDK's Zod validation would
|
|
6
|
+
* normally strip them.
|
|
7
|
+
*/
|
|
8
|
+
import { jest, describe, it, expect, beforeEach, afterEach } from "@jest/globals";
|
|
9
|
+
import { getToolsWithPreservedHints, } from "../../lib/assessment-runner/tools-with-hints.js";
|
|
10
|
+
describe("getToolsWithPreservedHints", () => {
|
|
11
|
+
let mockClient;
|
|
12
|
+
let mockTransport;
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
mockTransport = {
|
|
15
|
+
onmessage: undefined,
|
|
16
|
+
};
|
|
17
|
+
mockClient = {
|
|
18
|
+
transport: mockTransport,
|
|
19
|
+
listTools: jest.fn(),
|
|
20
|
+
};
|
|
21
|
+
});
|
|
22
|
+
afterEach(() => {
|
|
23
|
+
jest.clearAllMocks();
|
|
24
|
+
});
|
|
25
|
+
it("should preserve readOnlyHint from direct property in raw response", async () => {
|
|
26
|
+
// SDK returns tool without readOnlyHint (stripped by Zod)
|
|
27
|
+
mockClient.listTools.mockImplementation(async () => {
|
|
28
|
+
// Simulate raw response before SDK processes it
|
|
29
|
+
if (mockTransport.onmessage) {
|
|
30
|
+
mockTransport.onmessage({
|
|
31
|
+
result: {
|
|
32
|
+
tools: [
|
|
33
|
+
{
|
|
34
|
+
name: "browse_subreddit",
|
|
35
|
+
description: "Browse posts",
|
|
36
|
+
readOnlyHint: true, // Direct property in raw response
|
|
37
|
+
},
|
|
38
|
+
],
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
// Return SDK-processed tools (without direct readOnlyHint)
|
|
43
|
+
return {
|
|
44
|
+
tools: [
|
|
45
|
+
{
|
|
46
|
+
name: "browse_subreddit",
|
|
47
|
+
description: "Browse posts",
|
|
48
|
+
// readOnlyHint stripped by SDK Zod validation
|
|
49
|
+
},
|
|
50
|
+
],
|
|
51
|
+
};
|
|
52
|
+
});
|
|
53
|
+
const tools = await getToolsWithPreservedHints(mockClient);
|
|
54
|
+
expect(tools).toHaveLength(1);
|
|
55
|
+
expect(tools[0].name).toBe("browse_subreddit");
|
|
56
|
+
expect(tools[0].readOnlyHint).toBe(true);
|
|
57
|
+
});
|
|
58
|
+
it("should preserve all hint properties from raw response", async () => {
|
|
59
|
+
mockClient.listTools.mockImplementation(async () => {
|
|
60
|
+
if (mockTransport.onmessage) {
|
|
61
|
+
mockTransport.onmessage({
|
|
62
|
+
result: {
|
|
63
|
+
tools: [
|
|
64
|
+
{
|
|
65
|
+
name: "delete_item",
|
|
66
|
+
readOnlyHint: false,
|
|
67
|
+
destructiveHint: true,
|
|
68
|
+
idempotentHint: false,
|
|
69
|
+
openWorldHint: true,
|
|
70
|
+
},
|
|
71
|
+
],
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
return { tools: [{ name: "delete_item" }] };
|
|
76
|
+
});
|
|
77
|
+
const tools = await getToolsWithPreservedHints(mockClient);
|
|
78
|
+
expect(tools[0].readOnlyHint).toBe(false);
|
|
79
|
+
expect(tools[0].destructiveHint).toBe(true);
|
|
80
|
+
expect(tools[0].idempotentHint).toBe(false);
|
|
81
|
+
expect(tools[0].openWorldHint).toBe(true);
|
|
82
|
+
});
|
|
83
|
+
it("should not override SDK annotations with raw direct properties", async () => {
|
|
84
|
+
// If SDK has annotations, they take precedence
|
|
85
|
+
mockClient.listTools.mockImplementation(async () => {
|
|
86
|
+
if (mockTransport.onmessage) {
|
|
87
|
+
mockTransport.onmessage({
|
|
88
|
+
result: {
|
|
89
|
+
tools: [
|
|
90
|
+
{
|
|
91
|
+
name: "read_file",
|
|
92
|
+
readOnlyHint: false, // Raw says false
|
|
93
|
+
},
|
|
94
|
+
],
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
return {
|
|
99
|
+
tools: [
|
|
100
|
+
{
|
|
101
|
+
name: "read_file",
|
|
102
|
+
annotations: {
|
|
103
|
+
readOnlyHint: true, // SDK annotations say true
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
],
|
|
107
|
+
};
|
|
108
|
+
});
|
|
109
|
+
const tools = await getToolsWithPreservedHints(mockClient);
|
|
110
|
+
// SDK annotations should take precedence
|
|
111
|
+
expect(tools[0].annotations?.readOnlyHint).toBe(true);
|
|
112
|
+
// Direct property should NOT override
|
|
113
|
+
expect(tools[0].readOnlyHint).toBeUndefined();
|
|
114
|
+
});
|
|
115
|
+
it("should handle tools with no raw match gracefully", async () => {
|
|
116
|
+
mockClient.listTools.mockImplementation(async () => {
|
|
117
|
+
if (mockTransport.onmessage) {
|
|
118
|
+
mockTransport.onmessage({
|
|
119
|
+
result: {
|
|
120
|
+
tools: [
|
|
121
|
+
{ name: "tool_a", readOnlyHint: true },
|
|
122
|
+
// tool_b missing from raw
|
|
123
|
+
],
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
return {
|
|
128
|
+
tools: [
|
|
129
|
+
{ name: "tool_a" },
|
|
130
|
+
{ name: "tool_b" }, // Not in raw response
|
|
131
|
+
],
|
|
132
|
+
};
|
|
133
|
+
});
|
|
134
|
+
const tools = await getToolsWithPreservedHints(mockClient);
|
|
135
|
+
expect(tools).toHaveLength(2);
|
|
136
|
+
expect(tools[0].readOnlyHint).toBe(true);
|
|
137
|
+
expect(tools[1].readOnlyHint).toBeUndefined();
|
|
138
|
+
});
|
|
139
|
+
it("should fallback to SDK tools when transport not available", async () => {
|
|
140
|
+
mockClient.transport = null;
|
|
141
|
+
mockClient.listTools.mockImplementation(async () => ({
|
|
142
|
+
tools: [{ name: "test_tool", annotations: { readOnlyHint: true } }],
|
|
143
|
+
}));
|
|
144
|
+
const tools = await getToolsWithPreservedHints(mockClient);
|
|
145
|
+
expect(tools).toHaveLength(1);
|
|
146
|
+
expect(tools[0].name).toBe("test_tool");
|
|
147
|
+
});
|
|
148
|
+
it("should fallback to SDK tools when raw capture fails", async () => {
|
|
149
|
+
mockClient.listTools.mockImplementation(async () => {
|
|
150
|
+
// Don't trigger onmessage (simulating capture failure)
|
|
151
|
+
return {
|
|
152
|
+
tools: [{ name: "fallback_tool" }],
|
|
153
|
+
};
|
|
154
|
+
});
|
|
155
|
+
const tools = await getToolsWithPreservedHints(mockClient);
|
|
156
|
+
expect(tools).toHaveLength(1);
|
|
157
|
+
expect(tools[0].name).toBe("fallback_tool");
|
|
158
|
+
});
|
|
159
|
+
it("should preserve hints from metadata object", async () => {
|
|
160
|
+
mockClient.listTools.mockImplementation(async () => {
|
|
161
|
+
if (mockTransport.onmessage) {
|
|
162
|
+
mockTransport.onmessage({
|
|
163
|
+
result: {
|
|
164
|
+
tools: [
|
|
165
|
+
{
|
|
166
|
+
name: "query_db",
|
|
167
|
+
metadata: {
|
|
168
|
+
readOnlyHint: true,
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
],
|
|
172
|
+
},
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
return { tools: [{ name: "query_db" }] };
|
|
176
|
+
});
|
|
177
|
+
const tools = await getToolsWithPreservedHints(mockClient);
|
|
178
|
+
expect(tools[0].readOnlyHint).toBe(true);
|
|
179
|
+
});
|
|
180
|
+
it("should preserve hints from _meta object", async () => {
|
|
181
|
+
mockClient.listTools.mockImplementation(async () => {
|
|
182
|
+
if (mockTransport.onmessage) {
|
|
183
|
+
mockTransport.onmessage({
|
|
184
|
+
result: {
|
|
185
|
+
tools: [
|
|
186
|
+
{
|
|
187
|
+
name: "custom_tool",
|
|
188
|
+
_meta: {
|
|
189
|
+
destructiveHint: true,
|
|
190
|
+
},
|
|
191
|
+
},
|
|
192
|
+
],
|
|
193
|
+
},
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
return { tools: [{ name: "custom_tool" }] };
|
|
197
|
+
});
|
|
198
|
+
const tools = await getToolsWithPreservedHints(mockClient);
|
|
199
|
+
expect(tools[0].destructiveHint).toBe(true);
|
|
200
|
+
});
|
|
201
|
+
it("should restore original onmessage handler after call", async () => {
|
|
202
|
+
const originalHandler = jest.fn();
|
|
203
|
+
mockTransport.onmessage = originalHandler;
|
|
204
|
+
mockClient.listTools.mockImplementation(async () => {
|
|
205
|
+
if (mockTransport.onmessage) {
|
|
206
|
+
mockTransport.onmessage({ result: { tools: [] } });
|
|
207
|
+
}
|
|
208
|
+
return { tools: [] };
|
|
209
|
+
});
|
|
210
|
+
await getToolsWithPreservedHints(mockClient);
|
|
211
|
+
// Original handler should be restored
|
|
212
|
+
expect(mockTransport.onmessage).toBe(originalHandler);
|
|
213
|
+
});
|
|
214
|
+
it("should restore handler even on error", async () => {
|
|
215
|
+
const originalHandler = jest.fn();
|
|
216
|
+
mockTransport.onmessage = originalHandler;
|
|
217
|
+
mockClient.listTools.mockImplementation(async () => {
|
|
218
|
+
throw new Error("Test error");
|
|
219
|
+
});
|
|
220
|
+
await expect(getToolsWithPreservedHints(mockClient)).rejects.toThrow("Test error");
|
|
221
|
+
// Original handler should be restored despite error
|
|
222
|
+
expect(mockTransport.onmessage).toBe(originalHandler);
|
|
223
|
+
});
|
|
224
|
+
});
|
|
@@ -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", () => {
|
|
@@ -18,6 +18,8 @@ import { createCallToolWrapper } from "./tool-wrapper.js";
|
|
|
18
18
|
import { buildConfig } from "./config-builder.js";
|
|
19
19
|
// Issue #155: Import annotation debug mode setter
|
|
20
20
|
import { setAnnotationDebugMode } from "../../../../client/lib/services/assessment/modules/annotations/AlignmentChecker.js";
|
|
21
|
+
// Issue #155: Import helper to preserve hint properties stripped by SDK
|
|
22
|
+
import { getToolsWithPreservedHints } from "./tools-with-hints.js";
|
|
21
23
|
/**
|
|
22
24
|
* Run full assessment against an MCP server
|
|
23
25
|
*
|
|
@@ -65,9 +67,12 @@ export async function runFullAssessment(options) {
|
|
|
65
67
|
if (!serverInfo && !options.jsonOnly) {
|
|
66
68
|
console.log("⚠️ Server did not provide serverInfo during initialization");
|
|
67
69
|
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
+
// Issue #155: Use helper that preserves hint properties stripped by SDK Zod validation
|
|
71
|
+
// The SDK's listTools() validates against a schema that strips direct properties
|
|
72
|
+
// like readOnlyHint. This helper intercepts the raw transport response to preserve them.
|
|
73
|
+
const tools = await getToolsWithPreservedHints(client);
|
|
70
74
|
// Emit JSONL tool discovery events for audit-worker parsing
|
|
75
|
+
// Tools now have hint properties preserved from raw response
|
|
71
76
|
for (const tool of tools) {
|
|
72
77
|
emitToolDiscovered(tool);
|
|
73
78
|
}
|
|
@@ -283,9 +288,9 @@ export async function runFullAssessment(options) {
|
|
|
283
288
|
serverName: options.serverName,
|
|
284
289
|
tools,
|
|
285
290
|
callTool: createCallToolWrapper(client),
|
|
291
|
+
// Issue #155: Use helper to preserve hint properties in refreshed tool lists
|
|
286
292
|
listTools: async () => {
|
|
287
|
-
|
|
288
|
-
return response.tools;
|
|
293
|
+
return getToolsWithPreservedHints(client);
|
|
289
294
|
},
|
|
290
295
|
config,
|
|
291
296
|
sourceCodePath: options.sourceCodePath,
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tools With Preserved Hints
|
|
3
|
+
*
|
|
4
|
+
* Issue #155: The MCP SDK's listTools() method validates responses against
|
|
5
|
+
* a Zod schema that strips properties not explicitly defined (like readOnlyHint
|
|
6
|
+
* as a direct property on tools). This module intercepts the raw transport
|
|
7
|
+
* response to preserve these properties.
|
|
8
|
+
*
|
|
9
|
+
* @module cli/lib/assessment-runner/tools-with-hints
|
|
10
|
+
*/
|
|
11
|
+
// Hint property names we want to preserve
|
|
12
|
+
const HINT_PROPERTIES = [
|
|
13
|
+
"readOnlyHint",
|
|
14
|
+
"destructiveHint",
|
|
15
|
+
"idempotentHint",
|
|
16
|
+
"openWorldHint",
|
|
17
|
+
];
|
|
18
|
+
/**
|
|
19
|
+
* Get tools from MCP server with hint properties preserved.
|
|
20
|
+
*
|
|
21
|
+
* The MCP SDK's listTools() validates against a Zod schema that may strip
|
|
22
|
+
* direct hint properties (readOnlyHint, etc.) that aren't in the schema.
|
|
23
|
+
* This function intercepts the raw transport response to preserve them.
|
|
24
|
+
*
|
|
25
|
+
* @param client - Connected MCP client
|
|
26
|
+
* @returns Tools with hint properties preserved from raw response
|
|
27
|
+
*/
|
|
28
|
+
export async function getToolsWithPreservedHints(client) {
|
|
29
|
+
let rawTools = [];
|
|
30
|
+
// Get the underlying transport
|
|
31
|
+
const transport = client.transport;
|
|
32
|
+
if (!transport) {
|
|
33
|
+
// Fallback: just use SDK listTools if transport not accessible
|
|
34
|
+
const response = await client.listTools();
|
|
35
|
+
return response.tools || [];
|
|
36
|
+
}
|
|
37
|
+
// Store original message handler
|
|
38
|
+
const originalOnMessage = transport.onmessage;
|
|
39
|
+
// Intercept messages to capture raw tools/list response
|
|
40
|
+
transport.onmessage = (message, extra) => {
|
|
41
|
+
// Check if this is a tools/list response
|
|
42
|
+
const msg = message;
|
|
43
|
+
if (msg?.result?.tools && Array.isArray(msg.result.tools)) {
|
|
44
|
+
rawTools = msg.result.tools;
|
|
45
|
+
}
|
|
46
|
+
// Call original handler (cast to any to preserve original typing)
|
|
47
|
+
if (originalOnMessage) {
|
|
48
|
+
originalOnMessage(message, extra);
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
try {
|
|
52
|
+
// Make SDK call (triggers our interceptor)
|
|
53
|
+
const sdkResponse = await client.listTools();
|
|
54
|
+
const sdkTools = sdkResponse.tools || [];
|
|
55
|
+
// Restore original handler
|
|
56
|
+
transport.onmessage = originalOnMessage;
|
|
57
|
+
// If we didn't capture raw tools, just return SDK tools
|
|
58
|
+
if (rawTools.length === 0) {
|
|
59
|
+
return sdkTools;
|
|
60
|
+
}
|
|
61
|
+
// Merge preserved hint properties from raw into SDK tools
|
|
62
|
+
return sdkTools.map((sdkTool) => {
|
|
63
|
+
// Find matching raw tool by name
|
|
64
|
+
const rawTool = rawTools.find((rt) => rt.name === sdkTool.name);
|
|
65
|
+
if (!rawTool) {
|
|
66
|
+
return sdkTool;
|
|
67
|
+
}
|
|
68
|
+
// Start with SDK tool
|
|
69
|
+
const enrichedTool = { ...sdkTool };
|
|
70
|
+
// Preserve hint properties from raw response (priority order)
|
|
71
|
+
for (const hint of HINT_PROPERTIES) {
|
|
72
|
+
// Skip if SDK already has it via annotations
|
|
73
|
+
if (sdkTool.annotations?.[hint] !== undefined) {
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
// 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
|
|
81
|
+
let value;
|
|
82
|
+
if (typeof rawTool[hint] === "boolean") {
|
|
83
|
+
value = rawTool[hint];
|
|
84
|
+
}
|
|
85
|
+
else if (typeof rawTool.annotations?.[hint] === "boolean") {
|
|
86
|
+
value = rawTool.annotations[hint];
|
|
87
|
+
}
|
|
88
|
+
else if (typeof rawTool.metadata?.[hint] === "boolean") {
|
|
89
|
+
value = rawTool.metadata[hint];
|
|
90
|
+
}
|
|
91
|
+
else if (typeof rawTool._meta?.[hint] === "boolean") {
|
|
92
|
+
value = rawTool._meta[hint];
|
|
93
|
+
}
|
|
94
|
+
if (value !== undefined) {
|
|
95
|
+
enrichedTool[hint] = value;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return enrichedTool;
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
// Restore original handler on error
|
|
103
|
+
transport.onmessage = originalOnMessage;
|
|
104
|
+
throw error;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
@@ -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
|
|
67
|
-
|
|
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
|
|
70
|
-
destructiveHint
|
|
71
|
-
idempotentHint
|
|
72
|
-
openWorldHint
|
|
152
|
+
readOnlyHint,
|
|
153
|
+
destructiveHint,
|
|
154
|
+
idempotentHint,
|
|
155
|
+
openWorldHint,
|
|
73
156
|
}
|
|
74
157
|
: null;
|
|
75
158
|
emitJSONL({
|
package/package.json
CHANGED