Skip to content

Commit 7a072c0

Browse files
authored
build: rewrite docs generator (#640)
Closes #562
1 parent a29163d commit 7a072c0

File tree

3 files changed

+156
-57
lines changed

3 files changed

+156
-57
lines changed

scripts/generate-docs.ts

Lines changed: 122 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,12 @@
66

77
import fs from 'node:fs';
88

9-
import {Client} from '@modelcontextprotocol/sdk/client/index.js';
10-
import {StdioClientTransport} from '@modelcontextprotocol/sdk/client/stdio.js';
119
import type {Tool} from '@modelcontextprotocol/sdk/types.js';
1210

1311
import {cliOptions} from '../build/src/cli.js';
1412
import {ToolCategory, labels} from '../build/src/tools/categories.js';
13+
import {tools} from '../build/src/tools/tools.js';
1514

16-
const MCP_SERVER_PATH = 'build/src/index.js';
1715
const OUTPUT_PATH = './docs/tool-reference.md';
1816
const README_PATH = './README.md';
1917

@@ -25,6 +23,33 @@ interface ToolWithAnnotations extends Tool {
2523
};
2624
}
2725

26+
interface ZodCheck {
27+
kind: string;
28+
}
29+
30+
interface ZodDef {
31+
typeName: string;
32+
checks?: ZodCheck[];
33+
values?: string[];
34+
type?: ZodSchema;
35+
innerType?: ZodSchema;
36+
schema?: ZodSchema;
37+
defaultValue?: () => unknown;
38+
}
39+
40+
interface ZodSchema {
41+
_def: ZodDef;
42+
description?: string;
43+
}
44+
45+
interface TypeInfo {
46+
type: string;
47+
enum?: string[];
48+
items?: TypeInfo;
49+
description?: string;
50+
default?: unknown;
51+
}
52+
2853
function escapeHtmlTags(text: string): string {
2954
return text
3055
.replace(/&(?![a-zA-Z]+;)/g, '&')
@@ -162,34 +187,102 @@ function updateReadmeWithOptionsMarkdown(optionsMarkdown: string): void {
162187
console.log('Updated README.md with options markdown');
163188
}
164189

165-
async function generateToolDocumentation(): Promise<void> {
166-
console.log('Starting MCP server to query tool definitions...');
167-
168-
// Create MCP client with stdio transport pointing to the built server
169-
const transport = new StdioClientTransport({
170-
command: 'node',
171-
args: [MCP_SERVER_PATH, '--channel', 'canary'],
172-
});
173-
174-
const client = new Client(
175-
{
176-
name: 'docs-generator',
177-
version: '1.0.0',
178-
},
179-
{
180-
capabilities: {},
181-
},
182-
);
190+
// Helper to convert Zod schema to JSON schema-like object for docs
191+
function getZodTypeInfo(schema: ZodSchema): TypeInfo {
192+
let description = schema.description;
193+
let def = schema._def;
194+
let defaultValue: unknown;
195+
196+
// Unwrap optional/default/effects
197+
while (
198+
def.typeName === 'ZodOptional' ||
199+
def.typeName === 'ZodDefault' ||
200+
def.typeName === 'ZodEffects'
201+
) {
202+
if (def.typeName === 'ZodDefault' && def.defaultValue) {
203+
defaultValue = def.defaultValue();
204+
}
205+
const next = def.innerType || def.schema;
206+
if (!next) break;
207+
schema = next;
208+
def = schema._def;
209+
if (!description && schema.description) description = schema.description;
210+
}
183211

212+
const result: TypeInfo = {type: 'unknown'};
213+
if (description) result.description = description;
214+
if (defaultValue !== undefined) result.default = defaultValue;
215+
216+
switch (def.typeName) {
217+
case 'ZodString':
218+
result.type = 'string';
219+
break;
220+
case 'ZodNumber':
221+
result.type = def.checks?.some((c: ZodCheck) => c.kind === 'int')
222+
? 'integer'
223+
: 'number';
224+
break;
225+
case 'ZodBoolean':
226+
result.type = 'boolean';
227+
break;
228+
case 'ZodEnum':
229+
result.type = 'string';
230+
result.enum = def.values;
231+
break;
232+
case 'ZodArray':
233+
result.type = 'array';
234+
if (def.type) {
235+
result.items = getZodTypeInfo(def.type);
236+
}
237+
break;
238+
default:
239+
result.type = 'unknown';
240+
}
241+
return result;
242+
}
243+
244+
function isRequired(schema: ZodSchema): boolean {
245+
let def = schema._def;
246+
while (def.typeName === 'ZodEffects') {
247+
if (!def.schema) break;
248+
schema = def.schema;
249+
def = schema._def;
250+
}
251+
return def.typeName !== 'ZodOptional' && def.typeName !== 'ZodDefault';
252+
}
253+
254+
async function generateToolDocumentation(): Promise<void> {
184255
try {
185-
// Connect to the server
186-
await client.connect(transport);
187-
console.log('Connected to MCP server');
256+
console.log('Generating tool documentation from definitions...');
257+
258+
// Convert ToolDefinitions to ToolWithAnnotations
259+
const toolsWithAnnotations: ToolWithAnnotations[] = tools.map(tool => {
260+
const properties: Record<string, TypeInfo> = {};
261+
const required: string[] = [];
262+
263+
for (const [key, schema] of Object.entries(
264+
tool.schema as unknown as Record<string, ZodSchema>,
265+
)) {
266+
const info = getZodTypeInfo(schema);
267+
properties[key] = info;
268+
if (isRequired(schema)) {
269+
required.push(key);
270+
}
271+
}
272+
273+
return {
274+
name: tool.name,
275+
description: tool.description,
276+
inputSchema: {
277+
type: 'object',
278+
properties,
279+
required,
280+
},
281+
annotations: tool.annotations,
282+
};
283+
});
188284

189-
// List all available tools
190-
const {tools} = await client.listTools();
191-
const toolsWithAnnotations = tools as ToolWithAnnotations[];
192-
console.log(`Found ${tools.length} tools`);
285+
console.log(`Found ${toolsWithAnnotations.length} tools`);
193286

194287
// Generate markdown documentation
195288
let markdown = `<!-- AUTO GENERATED DO NOT EDIT - run 'npm run docs' to update-->
@@ -274,8 +367,7 @@ async function generateToolDocumentation(): Promise<void> {
274367

275368
const propertyNames = Object.keys(properties).sort();
276369
for (const propName of propertyNames) {
277-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
278-
const prop = properties[propName] as any;
370+
const prop = properties[propName] as TypeInfo;
279371
const isRequired = required.includes(propName);
280372
const requiredText = isRequired
281373
? ' **(required)**'
@@ -322,8 +414,6 @@ async function generateToolDocumentation(): Promise<void> {
322414
// Generate and update configuration options
323415
const optionsMarkdown = generateConfigOptionsMarkdown();
324416
updateReadmeWithOptionsMarkdown(optionsMarkdown);
325-
// Clean up
326-
await client.close();
327417
process.exit(0);
328418
} catch (error) {
329419
console.error('Error generating documentation:', error);

src/main.ts

Lines changed: 1 addition & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -21,16 +21,8 @@ import {
2121
SetLevelRequestSchema,
2222
} from './third_party/index.js';
2323
import {ToolCategory} from './tools/categories.js';
24-
import * as consoleTools from './tools/console.js';
25-
import * as emulationTools from './tools/emulation.js';
26-
import * as inputTools from './tools/input.js';
27-
import * as networkTools from './tools/network.js';
28-
import * as pagesTools from './tools/pages.js';
29-
import * as performanceTools from './tools/performance.js';
30-
import * as screenshotTools from './tools/screenshot.js';
31-
import * as scriptTools from './tools/script.js';
32-
import * as snapshotTools from './tools/snapshot.js';
3324
import type {ToolDefinition} from './tools/ToolDefinition.js';
25+
import {tools} from './tools/tools.js';
3426

3527
// If moved update release-please config
3628
// x-release-please-start-version
@@ -165,22 +157,6 @@ function registerTool(tool: ToolDefinition): void {
165157
);
166158
}
167159

168-
const tools = [
169-
...Object.values(consoleTools),
170-
...Object.values(emulationTools),
171-
...Object.values(inputTools),
172-
...Object.values(networkTools),
173-
...Object.values(pagesTools),
174-
...Object.values(performanceTools),
175-
...Object.values(screenshotTools),
176-
...Object.values(scriptTools),
177-
...Object.values(snapshotTools),
178-
] as ToolDefinition[];
179-
180-
tools.sort((a, b) => {
181-
return a.name.localeCompare(b.name);
182-
});
183-
184160
for (const tool of tools) {
185161
registerTool(tool);
186162
}

src/tools/tools.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/**
2+
* @license
3+
* Copyright 2025 Google LLC
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
import * as consoleTools from './console.js';
7+
import * as emulationTools from './emulation.js';
8+
import * as inputTools from './input.js';
9+
import * as networkTools from './network.js';
10+
import * as pagesTools from './pages.js';
11+
import * as performanceTools from './performance.js';
12+
import * as screenshotTools from './screenshot.js';
13+
import * as scriptTools from './script.js';
14+
import * as snapshotTools from './snapshot.js';
15+
import type {ToolDefinition} from './ToolDefinition.js';
16+
17+
const tools = [
18+
...Object.values(consoleTools),
19+
...Object.values(emulationTools),
20+
...Object.values(inputTools),
21+
...Object.values(networkTools),
22+
...Object.values(pagesTools),
23+
...Object.values(performanceTools),
24+
...Object.values(screenshotTools),
25+
...Object.values(scriptTools),
26+
...Object.values(snapshotTools),
27+
] as ToolDefinition[];
28+
29+
tools.sort((a, b) => {
30+
return a.name.localeCompare(b.name);
31+
});
32+
33+
export {tools};

0 commit comments

Comments
 (0)