Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
154 changes: 122 additions & 32 deletions scripts/generate-docs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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, '&')
Expand Down Expand Up @@ -162,34 +187,102 @@ function updateReadmeWithOptionsMarkdown(optionsMarkdown: string): void {
console.log('Updated README.md with options markdown');
}

async function generateToolDocumentation(): Promise<void> {
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<void> {
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<string, TypeInfo> = {};
const required: string[] = [];

for (const [key, schema] of Object.entries(
tool.schema as unknown as Record<string, ZodSchema>,
)) {
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 = `<!-- AUTO GENERATED DO NOT EDIT - run 'npm run docs' to update-->
Expand Down Expand Up @@ -274,8 +367,7 @@ async function generateToolDocumentation(): Promise<void> {

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)**'
Expand Down Expand Up @@ -322,8 +414,6 @@ async function generateToolDocumentation(): Promise<void> {
// 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);
Expand Down
26 changes: 1 addition & 25 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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);
}
Expand Down
33 changes: 33 additions & 0 deletions src/tools/tools.ts
Original file line number Diff line number Diff line change
@@ -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};