diff --git a/scripts/generate-docs.ts b/scripts/generate-docs.ts index 9a18c819..cec6c812 100644 --- a/scripts/generate-docs.ts +++ b/scripts/generate-docs.ts @@ -6,14 +6,12 @@ import fs from 'node:fs'; -import {Client} from '@modelcontextprotocol/sdk/client/index.js'; -import {StdioClientTransport} from '@modelcontextprotocol/sdk/client/stdio.js'; import type {Tool} from '@modelcontextprotocol/sdk/types.js'; import {cliOptions} from '../build/src/cli.js'; import {ToolCategory, labels} from '../build/src/tools/categories.js'; +import {tools} from '../build/src/tools/tools.js'; -const MCP_SERVER_PATH = 'build/src/index.js'; const OUTPUT_PATH = './docs/tool-reference.md'; const README_PATH = './README.md'; @@ -25,6 +23,33 @@ interface ToolWithAnnotations extends Tool { }; } +interface ZodCheck { + kind: string; +} + +interface ZodDef { + typeName: string; + checks?: ZodCheck[]; + values?: string[]; + type?: ZodSchema; + innerType?: ZodSchema; + schema?: ZodSchema; + defaultValue?: () => unknown; +} + +interface ZodSchema { + _def: ZodDef; + description?: string; +} + +interface TypeInfo { + type: string; + enum?: string[]; + items?: TypeInfo; + description?: string; + default?: unknown; +} + function escapeHtmlTags(text: string): string { return text .replace(/&(?![a-zA-Z]+;)/g, '&') @@ -162,34 +187,102 @@ function updateReadmeWithOptionsMarkdown(optionsMarkdown: string): void { console.log('Updated README.md with options markdown'); } -async function generateToolDocumentation(): Promise { - console.log('Starting MCP server to query tool definitions...'); - - // Create MCP client with stdio transport pointing to the built server - const transport = new StdioClientTransport({ - command: 'node', - args: [MCP_SERVER_PATH, '--channel', 'canary'], - }); - - const client = new Client( - { - name: 'docs-generator', - version: '1.0.0', - }, - { - capabilities: {}, - }, - ); +// Helper to convert Zod schema to JSON schema-like object for docs +function getZodTypeInfo(schema: ZodSchema): TypeInfo { + let description = schema.description; + let def = schema._def; + let defaultValue: unknown; + + // Unwrap optional/default/effects + while ( + def.typeName === 'ZodOptional' || + def.typeName === 'ZodDefault' || + def.typeName === 'ZodEffects' + ) { + if (def.typeName === 'ZodDefault' && def.defaultValue) { + defaultValue = def.defaultValue(); + } + const next = def.innerType || def.schema; + if (!next) break; + schema = next; + def = schema._def; + if (!description && schema.description) description = schema.description; + } + const result: TypeInfo = {type: 'unknown'}; + if (description) result.description = description; + if (defaultValue !== undefined) result.default = defaultValue; + + switch (def.typeName) { + case 'ZodString': + result.type = 'string'; + break; + case 'ZodNumber': + result.type = def.checks?.some((c: ZodCheck) => c.kind === 'int') + ? 'integer' + : 'number'; + break; + case 'ZodBoolean': + result.type = 'boolean'; + break; + case 'ZodEnum': + result.type = 'string'; + result.enum = def.values; + break; + case 'ZodArray': + result.type = 'array'; + if (def.type) { + result.items = getZodTypeInfo(def.type); + } + break; + default: + result.type = 'unknown'; + } + return result; +} + +function isRequired(schema: ZodSchema): boolean { + let def = schema._def; + while (def.typeName === 'ZodEffects') { + if (!def.schema) break; + schema = def.schema; + def = schema._def; + } + return def.typeName !== 'ZodOptional' && def.typeName !== 'ZodDefault'; +} + +async function generateToolDocumentation(): Promise { try { - // Connect to the server - await client.connect(transport); - console.log('Connected to MCP server'); + console.log('Generating tool documentation from definitions...'); + + // Convert ToolDefinitions to ToolWithAnnotations + const toolsWithAnnotations: ToolWithAnnotations[] = tools.map(tool => { + const properties: Record = {}; + const required: string[] = []; + + for (const [key, schema] of Object.entries( + tool.schema as unknown as Record, + )) { + const info = getZodTypeInfo(schema); + properties[key] = info; + if (isRequired(schema)) { + required.push(key); + } + } + + return { + name: tool.name, + description: tool.description, + inputSchema: { + type: 'object', + properties, + required, + }, + annotations: tool.annotations, + }; + }); - // List all available tools - const {tools} = await client.listTools(); - const toolsWithAnnotations = tools as ToolWithAnnotations[]; - console.log(`Found ${tools.length} tools`); + console.log(`Found ${toolsWithAnnotations.length} tools`); // Generate markdown documentation let markdown = ` @@ -274,8 +367,7 @@ async function generateToolDocumentation(): Promise { const propertyNames = Object.keys(properties).sort(); for (const propName of propertyNames) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const prop = properties[propName] as any; + const prop = properties[propName] as TypeInfo; const isRequired = required.includes(propName); const requiredText = isRequired ? ' **(required)**' @@ -322,8 +414,6 @@ async function generateToolDocumentation(): Promise { // Generate and update configuration options const optionsMarkdown = generateConfigOptionsMarkdown(); updateReadmeWithOptionsMarkdown(optionsMarkdown); - // Clean up - await client.close(); process.exit(0); } catch (error) { console.error('Error generating documentation:', error); diff --git a/src/main.ts b/src/main.ts index c19b23a5..14087243 100644 --- a/src/main.ts +++ b/src/main.ts @@ -21,16 +21,8 @@ import { SetLevelRequestSchema, } from './third_party/index.js'; import {ToolCategory} from './tools/categories.js'; -import * as consoleTools from './tools/console.js'; -import * as emulationTools from './tools/emulation.js'; -import * as inputTools from './tools/input.js'; -import * as networkTools from './tools/network.js'; -import * as pagesTools from './tools/pages.js'; -import * as performanceTools from './tools/performance.js'; -import * as screenshotTools from './tools/screenshot.js'; -import * as scriptTools from './tools/script.js'; -import * as snapshotTools from './tools/snapshot.js'; import type {ToolDefinition} from './tools/ToolDefinition.js'; +import {tools} from './tools/tools.js'; // If moved update release-please config // x-release-please-start-version @@ -165,22 +157,6 @@ function registerTool(tool: ToolDefinition): void { ); } -const tools = [ - ...Object.values(consoleTools), - ...Object.values(emulationTools), - ...Object.values(inputTools), - ...Object.values(networkTools), - ...Object.values(pagesTools), - ...Object.values(performanceTools), - ...Object.values(screenshotTools), - ...Object.values(scriptTools), - ...Object.values(snapshotTools), -] as ToolDefinition[]; - -tools.sort((a, b) => { - return a.name.localeCompare(b.name); -}); - for (const tool of tools) { registerTool(tool); } diff --git a/src/tools/tools.ts b/src/tools/tools.ts new file mode 100644 index 00000000..227fb0d4 --- /dev/null +++ b/src/tools/tools.ts @@ -0,0 +1,33 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ +import * as consoleTools from './console.js'; +import * as emulationTools from './emulation.js'; +import * as inputTools from './input.js'; +import * as networkTools from './network.js'; +import * as pagesTools from './pages.js'; +import * as performanceTools from './performance.js'; +import * as screenshotTools from './screenshot.js'; +import * as scriptTools from './script.js'; +import * as snapshotTools from './snapshot.js'; +import type {ToolDefinition} from './ToolDefinition.js'; + +const tools = [ + ...Object.values(consoleTools), + ...Object.values(emulationTools), + ...Object.values(inputTools), + ...Object.values(networkTools), + ...Object.values(pagesTools), + ...Object.values(performanceTools), + ...Object.values(screenshotTools), + ...Object.values(scriptTools), + ...Object.values(snapshotTools), +] as ToolDefinition[]; + +tools.sort((a, b) => { + return a.name.localeCompare(b.name); +}); + +export {tools};