Skip to content

Commit 5167cd3

Browse files
committed
cli
1 parent 6bc1049 commit 5167cd3

6 files changed

Lines changed: 62 additions & 21 deletions

File tree

src/cli.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,11 @@ export const cliOptions = {
157157
describe: 'Whether to enable vision tools',
158158
hidden: true,
159159
},
160+
experimentalStructuredContent: {
161+
type: 'boolean',
162+
describe: 'Whether to output structured formatted content.',
163+
hidden: true,
164+
},
160165
experimentalIncludeAllPages: {
161166
type: 'boolean',
162167
describe:
Lines changed: 13 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,20 +6,21 @@
66
import type {TextSnapshot, TextSnapshotNode} from '../McpContext.js';
77

88
export class SnapshotFormatter {
9-
constructor(public snapshot?: TextSnapshot) {}
9+
#snapshot: TextSnapshot;
10+
11+
constructor(snapshot: TextSnapshot) {
12+
this.#snapshot = snapshot;
13+
}
1014

1115
toString(): string {
12-
if (!this.snapshot) {
13-
return '';
14-
}
1516
const chunks: string[] = [];
16-
const root = this.snapshot.root;
17+
const root = this.#snapshot.root;
1718

1819
// Top-level content of the snapshot.
1920
if (
20-
this.snapshot.verbose &&
21-
this.snapshot.hasSelectedElement &&
22-
!this.snapshot.selectedElementUid
21+
this.#snapshot.verbose &&
22+
this.#snapshot.hasSelectedElement &&
23+
!this.#snapshot.selectedElementUid
2324
) {
2425
chunks.push(`Note: there is a selected element in the DevTools Elements panel but it is not included into the current a11y tree snapshot.
2526
Get a verbose snapshot to include all elements if you are interested in the selected element.\n\n`);
@@ -30,19 +31,16 @@ Get a verbose snapshot to include all elements if you are interested in the sele
3031
}
3132

3233
toJSON(): object {
33-
if (!this.snapshot) {
34-
return {};
35-
}
36-
return this.#nodeToJSON(this.snapshot.root);
34+
return this.#nodeToJSON(this.#snapshot.root);
3735
}
3836

39-
#formatNode(node: TextSnapshotNode, depth: number): string {
37+
#formatNode(node: TextSnapshotNode, depth = 0): string {
4038
const chunks: string[] = [];
4139
const attributes = this.#getAttributes(node);
4240
const line =
4341
' '.repeat(depth * 2) +
4442
attributes.join(' ') +
45-
(node.id === this.snapshot?.selectedElementUid
43+
(node.id === this.#snapshot.selectedElementUid
4644
? ' [selected in the DevTools Elements panel]'
4745
: '') +
4846
'\n';
@@ -57,9 +55,7 @@ Get a verbose snapshot to include all elements if you are interested in the sele
5755
#nodeToJSON(node: TextSnapshotNode): object {
5856
const rawAttrs = this.#getAttributesMap(node);
5957
const children = node.children.map(child => this.#nodeToJSON(child));
60-
const result: Record<string, unknown> = {
61-
...rawAttrs,
62-
};
58+
const result: Record<string, unknown> = structuredClone(rawAttrs);
6359
if (children.length > 0) {
6460
result.children = children;
6561
}

src/main.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -153,10 +153,22 @@ function registerTool(tool: ToolDefinition): void {
153153
response,
154154
context,
155155
);
156-
const {content} = await response.handle(tool.name, context);
157-
return {
156+
const {content, structuredContent} = await response.handle(
157+
tool.name,
158+
context,
159+
);
160+
const result: CallToolResult & {
161+
structuredContent?: Record<string, unknown>;
162+
} = {
158163
content,
159164
};
165+
if (args.experimentalStructuredContent) {
166+
result.structuredContent = structuredContent as Record<
167+
string,
168+
unknown
169+
>;
170+
}
171+
return result;
160172
} catch (err) {
161173
logger(`${tool.name} error:`, err, err?.stack);
162174
let errorText = err && 'message' in err ? err.message : String(err);

tests/McpResponse.test.js.snapshot

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ Saved snapshot to <file>
166166

167167
exports[`McpResponse > saves snapshot to file and returns structured content 2`] = `
168168
{
169-
"snapshotFilePath": "/var/folders/hq/m1wr43z9665g5pghys7gkc2w00r23k/T/test-screenshot.png"
169+
"snapshotFilePath": "<file>"
170170
}
171171
`;
172172

tests/McpResponse.test.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
getTextContent,
1919
html,
2020
stabilizeResponseOutput,
21+
stabilizeStructuredContent,
2122
withMcpContext,
2223
} from './utils.js';
2324

@@ -122,7 +123,13 @@ describe('McpResponse', () => {
122123
t.assert.snapshot?.(
123124
stabilizeResponseOutput(getTextContent(content[0])),
124125
);
125-
t.assert.snapshot?.(JSON.stringify(structuredContent, null, 2));
126+
t.assert.snapshot?.(
127+
JSON.stringify(
128+
stabilizeStructuredContent(structuredContent),
129+
null,
130+
2,
131+
),
132+
);
126133
});
127134
const content = await readFile(filePath, 'utf-8');
128135
t.assert.snapshot?.(stabilizeResponseOutput(content));

tests/utils.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,27 @@ export function html(
188188
</html>`;
189189
}
190190

191+
export function stabilizeStructuredContent(content: unknown): unknown {
192+
if (typeof content === 'string') {
193+
return stabilizeResponseOutput(content);
194+
}
195+
if (Array.isArray(content)) {
196+
return content.map(item => stabilizeStructuredContent(item));
197+
}
198+
if (typeof content === 'object' && content !== null) {
199+
const result: Record<string, unknown> = {};
200+
for (const [key, value] of Object.entries(content)) {
201+
if (key === 'snapshotFilePath' && typeof value === 'string') {
202+
result[key] = '<file>';
203+
} else {
204+
result[key] = stabilizeStructuredContent(value);
205+
}
206+
}
207+
return result;
208+
}
209+
return content;
210+
}
211+
191212
export function stabilizeResponseOutput(text: unknown) {
192213
if (typeof text !== 'string') {
193214
throw new Error('Input must be string');

0 commit comments

Comments
 (0)