#!/usr/bin/env node
/**
* Generate manifest.json tools and README.md tools table from ALL_TOOLS.
*
* This script reads the compiled tool definitions and updates:
* - package.json: description with tool count
* - manifest.json: tools array, description with tool count
* - README.md: intro line with tool count, tools table between markers
*
* Usage: node scripts/generate.mjs
*/
import { readFileSync, writeFileSync } from 'node:fs'
import { dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url'
const __dirname = dirname(fileURLToPath(import.meta.url))
const ROOT = join(__dirname, '..')
/**
* Load ALL_TOOLS from compiled output.
*/
async function loadTools() {
const { ALL_TOOLS } = await import(join(ROOT, 'dist/registry/tool-definitions.js'))
return ALL_TOOLS
}
/**
* Update package.json description with tool count.
*/
function updatePackageDescription(tools) {
const pkgPath = join(ROOT, 'package.json')
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'))
pkg.description = `${tools.length} tools for managing Grist documents with AI`
writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n')
console.log(`✓ Updated package.json description`)
}
/**
* Update manifest.json with tools and description from ALL_TOOLS.
*/
function updateManifest(tools) {
const manifestPath = join(ROOT, 'manifest.json')
const manifest = JSON.parse(readFileSync(manifestPath, 'utf-8'))
manifest.description = `${tools.length} tools for managing Grist documents with AI`
manifest.tools = tools.map((tool) => ({
name: tool.name,
description: tool.purpose
}))
manifest.tools_generated = true
writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + '\n')
console.log(`✓ Updated manifest.json with ${tools.length} tools`)
}
/**
* Update README.md intro line and tools table.
*/
function updateReadme(tools) {
const readmePath = join(ROOT, 'README.md')
let readme = readFileSync(readmePath, 'utf-8')
// Update intro line (line 7) with tool count
const introPattern = /^Model Context Protocol server.*$/m
const newIntro = `Model Context Protocol server with ${tools.length} tools for the Grist API.`
readme = readme.replace(introPattern, newIntro)
// Update tools table
const startMarker = '<!-- TOOLS_TABLE_START -->'
const endMarker = '<!-- TOOLS_TABLE_END -->'
const startIdx = readme.indexOf(startMarker)
const endIdx = readme.indexOf(endMarker)
if (startIdx === -1 || endIdx === -1) {
console.log('⚠ README.md missing TOOLS_TABLE markers, skipping tools table update')
writeFileSync(readmePath, readme)
console.log(`✓ Updated README.md intro line`)
return
}
// Build markdown table
const header = '| Tool | Purpose |\n|------|---------|'
const rows = tools.map((tool) => `| \`${tool.name}\` | ${tool.purpose} |`)
const table = `${startMarker}\n${header}\n${rows.join('\n')}\n${endMarker}`
const newReadme = readme.slice(0, startIdx) + table + readme.slice(endIdx + endMarker.length)
writeFileSync(readmePath, newReadme)
console.log(`✓ Updated README.md intro and tools table with ${tools.length} tools`)
}
/**
* Validate all tools have required documentation.
* Fails build if any tool is missing overview or examples.
*/
function validateDocumentation(tools) {
const missing = []
for (const tool of tools) {
const issues = []
if (!tool.docs) {
issues.push('missing docs')
} else {
if (!tool.docs.overview || tool.docs.overview.trim() === '') {
issues.push('missing overview')
}
if (!tool.docs.examples || tool.docs.examples.length === 0) {
issues.push('missing examples')
}
if (!tool.docs.errors || tool.docs.errors.length === 0) {
issues.push('missing errors')
}
}
if (issues.length > 0) {
missing.push({ name: tool.name, issues })
}
}
if (missing.length > 0) {
console.error('\n✗ Documentation validation failed:')
for (const { name, issues } of missing) {
console.error(` ${name}: ${issues.join(', ')}`)
}
console.error('\nAll tools must have docs.overview, docs.examples, and docs.errors')
process.exit(1)
}
console.log(`✓ Documentation validated for ${tools.length} tools`)
}
/**
* Generate tool-names.generated.ts from ALL_TOOLS.
* This creates a const tuple that can be used with z.enum().
*/
function generateToolNames(tools) {
const toolNamesPath = join(ROOT, 'src/schemas/tool-names.generated.ts')
const toolNames = tools.map((t) => t.name)
const content = `// AUTO-GENERATED by scripts/generate.mjs - DO NOT EDIT
// Run \`npm run build\` to regenerate
/**
* Tool names as a const tuple for use with z.enum().
* Generated from ALL_TOOLS at build time.
*/
export const TOOL_NAMES = [
${toolNames.map((n) => ` '${n}'`).join(',\n')}
] as const
export type ToolName = (typeof TOOL_NAMES)[number]
`
writeFileSync(toolNamesPath, content)
console.log(`✓ Generated tool-names.generated.ts with ${tools.length} tools`)
}
async function main() {
try {
console.log('Generating from ALL_TOOLS...\n')
const tools = await loadTools()
console.log(`Loaded ${tools.length} tools from tool-definitions.js\n`)
// Validate documentation first (fails build if missing)
validateDocumentation(tools)
updatePackageDescription(tools)
updateManifest(tools)
updateReadme(tools)
generateToolNames(tools)
console.log('\n✓ Generation complete')
} catch (error) {
console.error('Generation failed:', error.message)
process.exit(1)
}
}
main()