diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 12ee96ac4..067d20aeb 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -145,7 +145,5 @@ export const scenario: TestScenario = { ## Restrictions on JSON schema -- no .nullable(), no .object() types. +- no .nullable(), no .object() types. Enforced by the `@local/enforce-zod-schema` ESLint rule. - represent complex object as a short formatted string. - -TODO: implement eslint for schema https://github.com/ChromeDevTools/chrome-devtools-mcp/issues/1076 diff --git a/eslint.config.mjs b/eslint.config.mjs index b1a6121c6..ade8c4e8f 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -135,6 +135,13 @@ export default defineConfig([ ], }, }, + { + name: 'Tools definitions', + files: ['src/tools/**/*.ts'], + rules: { + '@local/enforce-zod-schema': 'error', + }, + }, { name: 'Tests', files: ['**/*.test.ts'], diff --git a/scripts/eslint_rules/enforce-zod-schema-rule.js b/scripts/eslint_rules/enforce-zod-schema-rule.js new file mode 100644 index 000000000..d75dfb11d --- /dev/null +++ b/scripts/eslint_rules/enforce-zod-schema-rule.js @@ -0,0 +1,62 @@ +/** + * @license + * Copyright 2026 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +export default { + name: 'enforce-zod-schema', + meta: { + type: 'problem', + docs: { + description: + 'Disallow .nullable() and .object() in tool schemas. Use optional strings to represent complex objects.', + }, + schema: [], + messages: { + noNullable: + 'Do not use .nullable() in tool schemas. Use .optional() instead.', + noObject: + 'Do not use .object() in tool schemas. Represent complex objects as a short formatted string.', + }, + }, + defaultOptions: [], + create(context) { + return { + CallExpression(node) { + if ( + node.callee.type !== 'MemberExpression' || + node.callee.property.type !== 'Identifier' + ) { + return; + } + + const methodName = node.callee.property.name; + + // We don't validate that .nullable() is called on a ZodObject + // specifically - this intentionally catches all .nullable() calls + // in tool schema files. + if (methodName === 'nullable') { + context.report({ + node: node.callee.property, + messageId: 'noNullable', + }); + } + + if (methodName === 'object') { + // Only flag zod.object() calls, not arbitrary .object() calls. + const obj = node.callee.object; + if ( + obj.type === 'Identifier' && + (obj.name === 'zod' || obj.name === 'z') + ) { + context.report({ + node: node.callee.property, + messageId: 'noObject', + }); + } + } + }, + }; + }, +}; diff --git a/scripts/eslint_rules/local-plugin.js b/scripts/eslint_rules/local-plugin.js index 27a20d372..b61d399b4 100644 --- a/scripts/eslint_rules/local-plugin.js +++ b/scripts/eslint_rules/local-plugin.js @@ -5,5 +5,11 @@ */ import checkLicenseRule from './check-license-rule.js'; +import enforceZodSchemaRule from './enforce-zod-schema-rule.js'; -export default {rules: {'check-license': checkLicenseRule}}; +export default { + rules: { + 'check-license': checkLicenseRule, + 'enforce-zod-schema': enforceZodSchemaRule, + }, +}; diff --git a/src/tools/input.ts b/src/tools/input.ts index 059652a17..1ddbcacd9 100644 --- a/src/tools/input.ts +++ b/src/tools/input.ts @@ -322,6 +322,7 @@ export const fillForm = definePageTool({ schema: { elements: zod .array( + // eslint-disable-next-line @local/enforce-zod-schema zod.object({ uid: zod.string().describe('The uid of the element to fill out'), value: zod.string().describe('Value for the element'),