Skip to content

Commit 8444eaa

Browse files
committed
chore: structured content for console
1 parent a5134c6 commit 8444eaa

5 files changed

Lines changed: 230 additions & 25 deletions

File tree

src/McpResponse.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -432,6 +432,8 @@ Call ${handleDialog.name} to handle it before continuing.`);
432432
tabId?: string;
433433
networkRequest?: object;
434434
networkRequests?: object[];
435+
detailedConsoleMessage?: object;
436+
consoleMessages?: object[];
435437
} = {};
436438

437439
if (this.#tabId) {
@@ -457,6 +459,10 @@ Call ${handleDialog.name} to handle it before continuing.`);
457459
response.push(
458460
...this.#formatConsoleData(context, data.detailedConsoleMessage),
459461
);
462+
if (data.detailedConsoleMessage) {
463+
structuredContent.detailedConsoleMessage =
464+
data.detailedConsoleMessage.toJSONDetailed();
465+
}
460466

461467
if (this.#networkRequestsOptions?.include) {
462468
let requests = context.getNetworkRequests(
@@ -511,6 +517,9 @@ Call ${handleDialog.name} to handle it before continuing.`);
511517
return message.toString();
512518
}),
513519
);
520+
structuredContent.consoleMessages = data.items.map(message =>
521+
message.toJSON(),
522+
);
514523
} else {
515524
response.push('<no console messages found>');
516525
}

src/formatters/ConsoleFormatter.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,4 +171,27 @@ export class ConsoleFormatter {
171171
}
172172
return result;
173173
}
174+
toJSON(): object {
175+
return {
176+
type: this.#getType(),
177+
text: this.#getText(),
178+
argsCount:
179+
this.#msg instanceof Error
180+
? 0
181+
: this.#resolvedArgs.length || this.#msg.args().length,
182+
id: this.#id,
183+
};
184+
}
185+
186+
toJSONDetailed(): object {
187+
return {
188+
id: this.#id,
189+
type: this.#getType(),
190+
text: this.#getText(),
191+
args: this.#getArgs().map(arg =>
192+
typeof arg === 'object' ? arg : String(arg),
193+
),
194+
stackTrace: this.#resolvedStackTrace,
195+
};
196+
}
174197
}

src/formatters/IssueFormatter.ts

Lines changed: 55 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,60 @@ export class IssueFormatter {
5959
}
6060
}
6161

62+
const issues = this.#issue.getAllIssues();
63+
const affectedResources = this.#getAffectedResources();
64+
if (affectedResources.length) {
65+
bodyParts.push('### Affected resources');
66+
bodyParts.push(
67+
...affectedResources.map(item => {
68+
const details = [];
69+
if (item.uid) {
70+
details.push(`uid=${item.uid}`);
71+
}
72+
if (item.request) {
73+
details.push(
74+
(typeof item.request === 'number' ? `reqid=` : 'url=') +
75+
item.request,
76+
);
77+
}
78+
if (item.data) {
79+
details.push(`data=${JSON.stringify(item.data)}`);
80+
}
81+
return details.join(' ');
82+
}),
83+
);
84+
}
85+
86+
result.push(`Message: issue> ${bodyParts.join('\n')}`);
87+
88+
return result.join('\n');
89+
}
90+
91+
toJSON(): object {
92+
return {
93+
type: 'issue',
94+
title: this.#getTitle(),
95+
count: this.#issue.getAggregatedIssuesCount(),
96+
id: this.#options.id,
97+
};
98+
}
99+
100+
toJSONDetailed(): object {
101+
return {
102+
id: this.#options.id,
103+
type: 'issue',
104+
title: this.#getTitle(),
105+
description: this.#getDescription(),
106+
links: this.#issue.getDescription()?.links,
107+
affectedResources: this.#getAffectedResources(),
108+
};
109+
}
110+
111+
#getAffectedResources(): Array<{
112+
uid?: string;
113+
data?: object;
114+
request?: string | number;
115+
}> {
62116
const issues = this.#issue.getAllIssues();
63117
const affectedResources: Array<{
64118
uid?: string;
@@ -125,31 +179,7 @@ export class IssueFormatter {
125179
request,
126180
});
127181
}
128-
if (affectedResources.length) {
129-
bodyParts.push('### Affected resources');
130-
bodyParts.push(
131-
...affectedResources.map(item => {
132-
const details = [];
133-
if (item.uid) {
134-
details.push(`uid=${item.uid}`);
135-
}
136-
if (item.request) {
137-
details.push(
138-
(typeof item.request === 'number' ? `reqid=` : 'url=') +
139-
item.request,
140-
);
141-
}
142-
if (item.data) {
143-
details.push(`data=${JSON.stringify(item.data)}`);
144-
}
145-
return details.join(' ');
146-
}),
147-
);
148-
}
149-
150-
result.push(`Message: issue> ${bodyParts.join('\n')}`);
151-
152-
return result.join('\n');
182+
return affectedResources;
153183
}
154184

155185
isValid(): boolean {

tests/formatters/ConsoleFormatter.test.ts

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
* SPDX-License-Identifier: Apache-2.0
55
*/
66

7+
import assert from 'node:assert';
78
import {describe, it} from 'node:test';
89

910
import {ConsoleFormatter} from '../../src/formatters/ConsoleFormatter.js';
@@ -163,4 +164,77 @@ describe('ConsoleFormatter', () => {
163164
t.assert.snapshot?.(result);
164165
});
165166
});
167+
describe('toJSON', () => {
168+
it('formats a console.log message', async () => {
169+
const message = createMockMessage({
170+
type: () => 'log',
171+
text: () => 'Hello, world!',
172+
});
173+
const result = (await ConsoleFormatter.from(message, {id: 1})).toJSON();
174+
assert.deepStrictEqual(result, {
175+
type: 'log',
176+
text: 'Hello, world!',
177+
argsCount: 0,
178+
id: 1,
179+
});
180+
});
181+
182+
it('formats a console.log message with args', async () => {
183+
const message = createMockMessage({
184+
type: () => 'log',
185+
text: () => 'Processing file:',
186+
args: () => [
187+
{jsonValue: async () => 'file.txt'},
188+
{jsonValue: async () => 'another file'},
189+
],
190+
});
191+
const result = (await ConsoleFormatter.from(message, {id: 1})).toJSON();
192+
assert.deepStrictEqual(result, {
193+
type: 'log',
194+
text: 'Processing file:',
195+
argsCount: 2,
196+
id: 1,
197+
});
198+
});
199+
});
200+
201+
describe('toJSONDetailed', () => {
202+
it('formats a console.log message', async () => {
203+
const message = createMockMessage({
204+
type: () => 'log',
205+
text: () => 'Hello, world!',
206+
});
207+
const result = (
208+
await ConsoleFormatter.from(message, {id: 1})
209+
).toJSONDetailed();
210+
assert.deepStrictEqual(result, {
211+
id: 1,
212+
type: 'log',
213+
text: 'Hello, world!',
214+
args: [],
215+
stackTrace: undefined,
216+
});
217+
});
218+
219+
it('formats a console.log message with args', async () => {
220+
const message = createMockMessage({
221+
type: () => 'log',
222+
text: () => 'Processing file:',
223+
args: () => [
224+
{jsonValue: async () => 'file.txt'},
225+
{jsonValue: async () => 'another file'},
226+
],
227+
});
228+
const result = (
229+
await ConsoleFormatter.from(message, {id: 2, fetchDetailedData: true})
230+
).toJSONDetailed();
231+
assert.deepStrictEqual(result, {
232+
id: 2,
233+
type: 'log',
234+
text: 'Processing file:',
235+
args: ['file.txt', 'another file'],
236+
stackTrace: undefined,
237+
});
238+
});
239+
});
166240
});

tests/formatters/IssueFormatter.test.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,4 +134,73 @@ describe('IssueFormatter', () => {
134134
assert.ok(detailed.includes('Valid Title'));
135135
});
136136
});
137+
describe('toJSON', () => {
138+
it('formats a simplified issue', () => {
139+
const mockAggregatedIssue = getMockAggregatedIssue();
140+
mockAggregatedIssue.getDescription.returns({
141+
file: 'mock.md',
142+
links: [],
143+
});
144+
mockAggregatedIssue.getAggregatedIssuesCount.returns(5);
145+
getIssueDescriptionStub
146+
.withArgs('mock.md')
147+
.returns('# Issue Title\n\nIssue content');
148+
149+
const formatter = new IssueFormatter(mockAggregatedIssue, {id: 1});
150+
assert.deepStrictEqual(formatter.toJSON(), {
151+
type: 'issue',
152+
title: 'Issue Title',
153+
count: 5,
154+
id: 1,
155+
});
156+
});
157+
});
158+
159+
describe('toJSONDetailed', () => {
160+
it('formats a detailed issue', () => {
161+
const testGenericIssue = {
162+
details: () => {
163+
return {
164+
violatingNodeId: 2,
165+
violatingNodeAttribute: 'test',
166+
};
167+
},
168+
};
169+
const mockAggregatedIssue = getMockAggregatedIssue();
170+
const mockDescription = {
171+
file: 'mock.md',
172+
links: [{link: 'http://example.com', linkTitle: 'Link 1'}],
173+
substitutions: new Map([['PLACEHOLDER_VALUE', 'sub value']]),
174+
};
175+
mockAggregatedIssue.getDescription.returns(mockDescription);
176+
177+
mockAggregatedIssue.getAllIssues.returns([testGenericIssue] as any);
178+
179+
const mockDescriptionFileContent =
180+
'# Mock Issue Title\n\nThis is a mock issue description {PLACEHOLDER_VALUE}';
181+
182+
getIssueDescriptionStub
183+
.withArgs('mock.md')
184+
.returns(mockDescriptionFileContent);
185+
186+
const formatter = new IssueFormatter(mockAggregatedIssue, {
187+
id: 5,
188+
});
189+
190+
const result = formatter.toJSONDetailed();
191+
assert.strictEqual((result as any).id, 5);
192+
assert.strictEqual((result as any).type, 'issue');
193+
assert.strictEqual((result as any).title, 'Mock Issue Title');
194+
assert.strictEqual(
195+
(result as any).description,
196+
'# Mock Issue Title\n\nThis is a mock issue description sub value',
197+
);
198+
assert.deepStrictEqual((result as any).links, mockDescription.links);
199+
assert.strictEqual((result as any).affectedResources.length, 1);
200+
assert.deepStrictEqual((result as any).affectedResources[0].data, {
201+
violatingNodeAttribute: 'test',
202+
violatingNodeId: 2,
203+
});
204+
});
205+
});
137206
});

0 commit comments

Comments
 (0)