Skip to content

Commit fd1a273

Browse files
mvanhornOrKoN
authored andcommitted
feat: add eslint rule to validate tool schema patterns
Add `@local/no-zod-nullable-object` ESLint rule that disallows `.nullable()` and `.object()` in tool schemas (src/tools/**). Fix existing violation in fill_form by using "uid=value" string format instead of zod.object(). Remove the TODO from CONTRIBUTING.md. Closes #1076
1 parent 73e1e24 commit fd1a273

6 files changed

Lines changed: 89 additions & 28 deletions

File tree

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/no-zod-nullable-object` 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
@@ -135,6 +135,13 @@ export default defineConfig([
135135
],
136136
},
137137
},
138+
{
139+
name: 'Tool schema restrictions',
140+
files: ['src/tools/**/*.ts'],
141+
rules: {
142+
'@local/no-zod-nullable-object': 'error',
143+
},
144+
},
138145
{
139146
name: 'Tests',
140147
files: ['**/*.test.ts'],

scripts/eslint_rules/local-plugin.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,11 @@
55
*/
66

77
import checkLicenseRule from './check-license-rule.js';
8+
import noZodNullableObjectRule from './no-zod-nullable-object-rule.js';
89

9-
export default {rules: {'check-license': checkLicenseRule}};
10+
export default {
11+
rules: {
12+
'check-license': checkLicenseRule,
13+
'no-zod-nullable-object': noZodNullableObjectRule,
14+
},
15+
};
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/**
2+
* @license
3+
* Copyright 2025 Google LLC
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
export default {
8+
name: 'no-zod-nullable-object',
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+
if (methodName === 'nullable') {
37+
context.report({
38+
node: node.callee.property,
39+
messageId: 'noNullable',
40+
});
41+
}
42+
43+
if (methodName === 'object') {
44+
// Only flag zod.object() calls, not arbitrary .object() calls.
45+
const obj = node.callee.object;
46+
if (
47+
obj.type === 'Identifier' &&
48+
(obj.name === 'zod' || obj.name === 'z')
49+
) {
50+
context.report({
51+
node: node.callee.property,
52+
messageId: 'noObject',
53+
});
54+
}
55+
}
56+
},
57+
};
58+
},
59+
};

src/tools/input.ts

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -321,25 +321,25 @@ export const fillForm = definePageTool({
321321
},
322322
schema: {
323323
elements: zod
324-
.array(
325-
zod.object({
326-
uid: zod.string().describe('The uid of the element to fill out'),
327-
value: zod.string().describe('Value for the element'),
328-
}),
329-
)
330-
.describe('Elements from snapshot to fill out.'),
324+
.array(zod.string())
325+
.describe(
326+
'Elements from snapshot to fill out. Each entry is formatted as "uid=value".',
327+
),
331328
includeSnapshot: includeSnapshotSchema,
332329
},
333330
handler: async (request, response, context) => {
334331
const page = request.page;
335-
for (const element of request.params.elements) {
336-
await context.waitForEventsAfterAction(async () => {
337-
await fillFormElement(
338-
element.uid,
339-
element.value,
340-
context as McpContext,
341-
page,
332+
for (const entry of request.params.elements) {
333+
const separatorIndex = entry.indexOf('=');
334+
if (separatorIndex === -1) {
335+
throw new Error(
336+
`Invalid element format: "${entry}". Expected "uid=value".`,
342337
);
338+
}
339+
const uid = entry.slice(0, separatorIndex);
340+
const value = entry.slice(separatorIndex + 1);
341+
await context.waitForEventsAfterAction(async () => {
342+
await fillFormElement(uid, value, context as McpContext, page);
343343
});
344344
}
345345
response.appendResponseLine(`Successfully filled out the form`);

tests/tools/input.test.ts

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -670,16 +670,7 @@ describe('input', () => {
670670
await fillForm.handler(
671671
{
672672
params: {
673-
elements: [
674-
{
675-
uid: '1_2',
676-
value: 'test',
677-
},
678-
{
679-
uid: '1_4',
680-
value: 'test2',
681-
},
682-
],
673+
elements: ['1_2=test', '1_4=test2'],
683674
},
684675
page: context.getSelectedMcpPage(),
685676
},

0 commit comments

Comments
 (0)