Skip to content

Commit 8738aa0

Browse files
committed
chore: fix
2 parents d747b37 + 2e1eaba commit 8738aa0

11 files changed

Lines changed: 331 additions & 180 deletions

CONTRIBUTING.md

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,5 @@ export const scenario: TestScenario = {
145145

146146
## Restrictions on JSON schema
147147

148-
- no .nullable(), no .object() types.
148+
- no .nullable(), no .object() types. Enforced by the `@local/enforce-zod-schema` ESLint rule.
149149
- represent complex object as a short formatted string.
150-
151-
TODO: implement eslint for schema https://github.com/ChromeDevTools/chrome-devtools-mcp/issues/1076

eslint.config.mjs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,13 @@ export default defineConfig([
142142
'@local/no-direct-third-party-imports': 'error',
143143
},
144144
},
145+
{
146+
name: 'Tools definitions',
147+
files: ['src/tools/**/*.ts'],
148+
rules: {
149+
'@local/enforce-zod-schema': 'error',
150+
},
151+
},
145152
{
146153
name: 'Tests',
147154
files: ['**/*.test.ts'],
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
/**
2+
* @license
3+
* Copyright 2026 Google LLC
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
export default {
8+
name: 'enforce-zod-schema',
9+
meta: {
10+
type: 'problem',
11+
docs: {
12+
description:
13+
'Disallow .nullable() and .object() in tool schemas. Use optional strings to represent complex objects.',
14+
},
15+
schema: [],
16+
messages: {
17+
noNullable:
18+
'Do not use .nullable() in tool schemas. Use .optional() instead.',
19+
noObject:
20+
'Do not use .object() in tool schemas. Represent complex objects as a short formatted string.',
21+
},
22+
},
23+
defaultOptions: [],
24+
create(context) {
25+
return {
26+
CallExpression(node) {
27+
if (
28+
node.callee.type !== 'MemberExpression' ||
29+
node.callee.property.type !== 'Identifier'
30+
) {
31+
return;
32+
}
33+
34+
const methodName = node.callee.property.name;
35+
36+
// We don't validate that .nullable() is called on a ZodObject
37+
// specifically - this intentionally catches all .nullable() calls
38+
// in tool schema files.
39+
if (methodName === 'nullable') {
40+
context.report({
41+
node: node.callee.property,
42+
messageId: 'noNullable',
43+
});
44+
}
45+
46+
if (methodName === 'object') {
47+
// Only flag zod.object() calls, not arbitrary .object() calls.
48+
const obj = node.callee.object;
49+
if (
50+
obj.type === 'Identifier' &&
51+
(obj.name === 'zod' || obj.name === 'z')
52+
) {
53+
context.report({
54+
node: node.callee.property,
55+
messageId: 'noObject',
56+
});
57+
}
58+
}
59+
},
60+
};
61+
},
62+
};

scripts/eslint_rules/local-plugin.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,13 @@
55
*/
66

77
import checkLicenseRule from './check-license-rule.js';
8+
import enforceZodSchemaRule from './enforce-zod-schema-rule.js';
89
import noDirectThirdPartyImportsRule from './no-direct-third-party-imports-rule.js';
910

1011
export default {
1112
rules: {
1213
'check-license': checkLicenseRule,
1314
'no-direct-third-party-imports': noDirectThirdPartyImportsRule,
15+
'enforce-zod-schema': enforceZodSchemaRule,
1416
},
1517
};

src/tools/input.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,7 @@ export const fillForm = definePageTool({
322322
schema: {
323323
elements: zod
324324
.array(
325+
// eslint-disable-next-line @local/enforce-zod-schema
325326
zod.object({
326327
uid: zod.string().describe('The uid of the element to fill out'),
327328
value: zod.string().describe('Value for the element'),
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
/**
2+
* @license
3+
* Copyright 2026 Google LLC
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
import assert from 'node:assert';
8+
import {describe, it, afterEach, beforeEach} from 'node:test';
9+
10+
import {
11+
assertDaemonIsNotRunning,
12+
assertDaemonIsRunning,
13+
runCli,
14+
} from '../utils.js';
15+
16+
describe('chrome-devtools', () => {
17+
beforeEach(async () => {
18+
await runCli(['stop']);
19+
await assertDaemonIsNotRunning();
20+
});
21+
22+
afterEach(async () => {
23+
await runCli(['stop']);
24+
await assertDaemonIsNotRunning();
25+
});
26+
27+
it('can invoke list_pages', async () => {
28+
await assertDaemonIsNotRunning();
29+
30+
const startResult = await runCli(['start']);
31+
assert.strictEqual(
32+
startResult.status,
33+
0,
34+
`start command failed: ${startResult.stderr}`,
35+
);
36+
37+
const listPagesResult = await runCli(['list_pages']);
38+
assert.strictEqual(
39+
listPagesResult.status,
40+
0,
41+
`list_pages command failed: ${listPagesResult.stderr}`,
42+
);
43+
assert(
44+
listPagesResult.stdout.includes('about:blank'),
45+
'list_pages output is unexpected',
46+
);
47+
48+
await assertDaemonIsRunning();
49+
});
50+
51+
it('can take screenshot', async () => {
52+
const startResult = await runCli(['start']);
53+
assert.strictEqual(
54+
startResult.status,
55+
0,
56+
`start command failed: ${startResult.stderr}`,
57+
);
58+
59+
const result = await runCli(['take_screenshot']);
60+
assert.strictEqual(
61+
result.status,
62+
0,
63+
`take_screenshot command failed: ${result.stderr}`,
64+
);
65+
assert(
66+
result.stdout.includes('.png'),
67+
'take_screenshot output is unexpected',
68+
);
69+
});
70+
});
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/**
2+
* @license
3+
* Copyright 2026 Google LLC
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
import assert from 'node:assert';
8+
import {describe, it, afterEach, beforeEach} from 'node:test';
9+
10+
import {assertDaemonIsNotRunning, runCli} from '../utils.js';
11+
12+
describe('chrome-devtools', () => {
13+
beforeEach(async () => {
14+
await runCli(['stop']);
15+
await assertDaemonIsNotRunning();
16+
});
17+
18+
afterEach(async () => {
19+
await runCli(['stop']);
20+
await assertDaemonIsNotRunning();
21+
});
22+
23+
it('forwards disclaimers to stderr on start', async () => {
24+
const result = await runCli(['start']);
25+
assert.strictEqual(
26+
result.status,
27+
0,
28+
`start command failed: ${result.stderr}`,
29+
);
30+
assert(
31+
result.stderr.includes('chrome-devtools-mcp exposes content'),
32+
'Disclaimer not found in stderr on start',
33+
);
34+
});
35+
});
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/**
2+
* @license
3+
* Copyright 2026 Google LLC
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
import assert from 'node:assert';
8+
import fs from 'node:fs';
9+
import os from 'node:os';
10+
import path from 'node:path';
11+
import {describe, it, afterEach, beforeEach} from 'node:test';
12+
13+
import {
14+
assertDaemonIsNotRunning,
15+
assertDaemonIsRunning,
16+
runCli,
17+
} from '../utils.js';
18+
19+
describe('chrome-devtools', () => {
20+
beforeEach(async () => {
21+
await runCli(['stop']);
22+
await assertDaemonIsNotRunning();
23+
});
24+
25+
afterEach(async () => {
26+
await runCli(['stop']);
27+
await assertDaemonIsNotRunning();
28+
});
29+
30+
it('can start and stop the daemon', async () => {
31+
await assertDaemonIsNotRunning();
32+
33+
const startResult = await runCli(['start']);
34+
assert.strictEqual(
35+
startResult.status,
36+
0,
37+
`start command failed: ${startResult.stderr}`,
38+
);
39+
40+
await assertDaemonIsRunning();
41+
42+
const stopResult = await runCli(['stop']);
43+
assert.strictEqual(
44+
stopResult.status,
45+
0,
46+
`stop command failed: ${stopResult.stderr}`,
47+
);
48+
49+
await assertDaemonIsNotRunning();
50+
});
51+
52+
it('can start the daemon with userDataDir', async () => {
53+
const userDataDir = path.join(
54+
os.tmpdir(),
55+
`chrome-devtools-test-${crypto.randomUUID()}`,
56+
);
57+
fs.mkdirSync(userDataDir, {recursive: true});
58+
59+
const startResult = await runCli(['start', '--userDataDir', userDataDir]);
60+
assert.strictEqual(
61+
startResult.status,
62+
0,
63+
`start command failed: ${startResult.stderr}`,
64+
);
65+
assert.ok(
66+
!startResult.stderr.includes(
67+
'Arguments userDataDir and isolated are mutually exclusive',
68+
),
69+
`unexpected conflict error: ${startResult.stderr}`,
70+
);
71+
72+
await assertDaemonIsRunning();
73+
});
74+
});
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/**
2+
* @license
3+
* Copyright 2026 Google LLC
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
import assert from 'node:assert';
8+
import {describe, it, afterEach, beforeEach} from 'node:test';
9+
10+
import {
11+
assertDaemonIsNotRunning,
12+
assertDaemonIsRunning,
13+
runCli,
14+
} from '../utils.js';
15+
16+
describe('chrome-devtools', () => {
17+
beforeEach(async () => {
18+
await runCli(['stop']);
19+
await assertDaemonIsNotRunning();
20+
});
21+
22+
afterEach(async () => {
23+
await runCli(['stop']);
24+
await assertDaemonIsNotRunning();
25+
});
26+
27+
it('reports daemon status correctly', async () => {
28+
await assertDaemonIsNotRunning();
29+
30+
const startResult = await runCli(['start']);
31+
assert.strictEqual(
32+
startResult.status,
33+
0,
34+
`start command failed: ${startResult.stderr}`,
35+
);
36+
37+
await assertDaemonIsRunning();
38+
});
39+
});

0 commit comments

Comments
 (0)