2026-06-06 20:12:40 +07:00
parent de84b2bf48
commit 97ac0f71f5
13682 changed files with 1125938 additions and 0 deletions
@@ -0,0 +1,300 @@
#!/usr/bin/env node
/**
* generate-simple-registry.js
*
* Generates tool-registry.json WITHOUT embeddings (no API key required)
* Now includes inputSchema for LLM tool usage
*
* Usage:
* node scripts/generate-simple-registry.js
*/
import { detectCategory } from '../utils/tool-loader.js';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
/**
* Parse zod schema string into JSON Schema
*/
function parseZodSchema(zodString) {
if (!zodString) return null;
const schema = {
type: 'object',
properties: {},
required: []
};
// Match property definitions like: name: z.string().describe('...')
// Handle nested objects and various zod methods
const lines = zodString.split('\n');
let braceDepth = 0;
for (let li = 0; li < lines.length; li++) {
const line = lines[li];
const trimmed = line.trim();
// Track brace depth for nested objects
braceDepth += (trimmed.match(/\{/g) || []).length;
braceDepth -= (trimmed.match(/\}/g) || []).length;
// Match top-level property: propertyName: z.type(...)
const propMatch = trimmed.match(/^(\w+):\s*z\.(\w+)\(/);
if (propMatch && braceDepth <= 1) {
const [, propName, zodType] = propMatch;
// Skip 'inputSchema' itself - we're parsing its contents
if (propName === 'inputSchema') continue;
const prop = {};
// Build "lookahead window" — if the property continues to next lines
// (multi-line z.enum/z.union/etc), accumulate until parens balance.
// This lets us catch .describe() that appears on a later line.
let window = line;
let parenDepth = (line.match(/\(/g) || []).length - (line.match(/\)/g) || []).length;
let extraLines = 0;
// Continue while: parens unbalanced OR next line is a chain continuation (.method)
while ((li + extraLines + 1) < lines.length && extraLines < 30) {
const nextLine = lines[li + extraLines + 1];
const nextTrim = nextLine.trim();
const isChainContinuation = nextTrim.startsWith('.');
if (parenDepth > 0 || isChainContinuation) {
extraLines++;
window += '\n' + nextLine;
parenDepth += (nextLine.match(/\(/g) || []).length;
parenDepth -= (nextLine.match(/\)/g) || []).length;
} else {
break;
}
}
const isOptional = /\.optional\(\)/.test(window);
// Extract description (single quote, double quote, or backtick)
let descMatch = window.match(/\.describe\(\s*'((?:[^']|\\')*)'\s*\)/);
if (!descMatch) descMatch = window.match(/\.describe\(\s*"((?:[^"]|\\")*)"\s*\)/);
if (!descMatch) descMatch = window.match(/\.describe\(\s*`([\s\S]*?)`\s*\)/);
if (descMatch) prop.description = descMatch[1].split('\n')[0].trim();
// Extract default value
const defaultMatch = window.match(/\.default\(([^)]+)\)/);
if (defaultMatch) {
try {
const defaultVal = defaultMatch[1].trim();
if (defaultVal.startsWith("'") || defaultVal.startsWith('"')) {
prop.default = defaultVal.slice(1, -1);
} else if (defaultVal === 'true' || defaultVal === 'false') {
prop.default = defaultVal === 'true';
} else if (!isNaN(Number(defaultVal))) {
prop.default = Number(defaultVal);
} else {
prop.default = defaultVal;
}
} catch {}
}
// Map zod types to JSON Schema
switch (zodType) {
case 'string':
prop.type = 'string';
break;
case 'number':
prop.type = 'number';
break;
case 'boolean':
prop.type = 'boolean';
break;
case 'enum':
prop.type = 'string';
const enumMatch = window.match(/z\.enum\(\[([\s\S]+?)\]\)/);
if (enumMatch) {
prop.enum = enumMatch[1].split(',')
.map(s => s.trim().replace(/['"`]/g, ''))
.filter(s => s.length > 0);
}
break;
case 'object':
prop.type = 'object';
if (window.includes('x: z.number()') || window.includes('x:z.number()')) {
prop.properties = {
x: { type: 'number' },
y: { type: 'number' },
z: { type: 'number' }
};
}
break;
case 'array':
prop.type = 'array';
break;
case 'union':
prop.type = 'string';
break;
default:
prop.type = 'string';
}
schema.properties[propName] = prop;
if (!isOptional && !defaultMatch) {
schema.required.push(propName);
}
}
}
// Remove empty required array
if (schema.required.length === 0) {
delete schema.required;
}
return Object.keys(schema.properties).length > 0 ? schema : null;
}
/**
* Extract inputSchema block from tool definition
*/
function extractInputSchema(content, toolStartIndex) {
// Find inputSchema: z.object({ starting from toolStartIndex
const searchArea = content.substring(toolStartIndex, toolStartIndex + 3000);
const schemaStart = searchArea.indexOf('inputSchema: z.object({');
if (schemaStart === -1) return null;
// Find matching closing brace
let braceCount = 0;
let inSchema = false;
let schemaEnd = schemaStart;
for (let i = schemaStart; i < searchArea.length; i++) {
const char = searchArea[i];
if (char === '{') {
braceCount++;
inSchema = true;
} else if (char === '}') {
braceCount--;
if (inSchema && braceCount === 0) {
schemaEnd = i + 1;
break;
}
}
}
const schemaBlock = searchArea.substring(schemaStart, schemaEnd);
return parseZodSchema(schemaBlock);
}
// Load existing index.js to extract tool definitions
function extractToolsFromIndexJs() {
const indexPath = path.join(__dirname, '..', 'index.js');
const content = fs.readFileSync(indexPath, 'utf-8');
const tools = [];
// Find all registerTool calls with their positions
const registerRegex = /mcpServer\.registerTool\('([^']+)',\s*\{/g;
let match;
while ((match = registerRegex.exec(content)) !== null) {
const name = match[1];
const startIndex = match.index;
// Extract title and description from the block after this match
const blockArea = content.substring(startIndex, startIndex + 2000);
// Title
const titleMatch = blockArea.match(/title:\s*'([^']+)'/);
const title = titleMatch ? titleMatch[1] : name;
// Description (single quote or backtick)
let description = '';
const descSingleMatch = blockArea.match(/description:\s*'([^']+)'/);
const descBacktickMatch = blockArea.match(/description:\s*`([^`]+)`/);
if (descSingleMatch) {
description = descSingleMatch[1].substring(0, 500);
} else if (descBacktickMatch) {
description = descBacktickMatch[1].split('\n')[0].trim().substring(0, 500);
}
// Extract inputSchema
const inputSchema = extractInputSchema(content, startIndex);
tools.push({
name,
title,
description,
inputSchema
});
}
// Remove duplicates
const uniqueTools = [];
const seenNames = new Set();
for (const tool of tools) {
if (!seenNames.has(tool.name)) {
seenNames.add(tool.name);
uniqueTools.push(tool);
}
}
console.log(`[Generator] Found ${uniqueTools.length} tools in index.js`);
return uniqueTools;
}
function generateRegistry() {
console.log('[Generator] Starting simple tool registry generation (with inputSchema)...');
const tools = extractToolsFromIndexJs();
if (tools.length === 0) {
console.error('[Generator] ERROR: No tools found in index.js');
process.exit(1);
}
const registry = {};
let schemaCount = 0;
for (const tool of tools) {
// Detect category
const category = detectCategory(tool.name);
registry[tool.name] = {
title: tool.title,
description: tool.description,
category: category,
inputSchema: tool.inputSchema || null,
embedding: null
};
if (tool.inputSchema) {
schemaCount++;
}
}
// Write to file
const outputPath = path.join(__dirname, '..', 'tool-registry.json');
fs.writeFileSync(outputPath, JSON.stringify(registry, null, 2));
console.log(`[Generator] ✅ Successfully generated tool-registry.json`);
console.log(`[Generator] Location: ${outputPath}`);
console.log(`[Generator] Total tools: ${Object.keys(registry).length}`);
console.log(`[Generator] Tools with inputSchema: ${schemaCount}`);
// Category breakdown
const categoryCount = {};
for (const meta of Object.values(registry)) {
categoryCount[meta.category] = (categoryCount[meta.category] || 0) + 1;
}
console.log('[Generator] Category breakdown:');
for (const [category, count] of Object.entries(categoryCount).sort((a, b) => b[1] - a[1])) {
console.log(` ${category}: ${count} tools`);
}
}
generateRegistry();