mirror of https://github.com/garrytan/gstack.git
Merge 530f5ad4ca into 2beb636f7c
This commit is contained in:
commit
2eece2f0a6
|
|
@ -0,0 +1,126 @@
|
||||||
|
/**
|
||||||
|
* MCP tool manifest generator for gstack browse commands.
|
||||||
|
*
|
||||||
|
* Generates a Model Context Protocol-compatible tool manifest from
|
||||||
|
* COMMAND_DESCRIPTIONS — the single source of truth for all browse commands.
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* bun run browse/src/mcp-manifest.ts → print JSON manifest
|
||||||
|
* bun run browse/src/mcp-manifest.ts --write → write to browse/mcp-tools.json
|
||||||
|
*
|
||||||
|
* The manifest enables any MCP-compatible agent (Claude Code, Codex, Gemini,
|
||||||
|
* Cursor) to discover and call gstack browse commands as tools.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { COMMAND_DESCRIPTIONS, READ_COMMANDS, WRITE_COMMANDS, META_COMMANDS } from './commands';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
|
||||||
|
interface MCPToolParameter {
|
||||||
|
type: string;
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MCPTool {
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object';
|
||||||
|
properties: Record<string, MCPToolParameter>;
|
||||||
|
required: string[];
|
||||||
|
};
|
||||||
|
annotations?: {
|
||||||
|
category: string;
|
||||||
|
readOnly: boolean;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse a usage string like "goto <url>" into parameter definitions.
|
||||||
|
* Handles: <required>, [optional], flags like --errors.
|
||||||
|
*/
|
||||||
|
function parseUsageParams(usage: string | undefined, command: string): {
|
||||||
|
properties: Record<string, MCPToolParameter>;
|
||||||
|
required: string[];
|
||||||
|
} {
|
||||||
|
const properties: Record<string, MCPToolParameter> = {};
|
||||||
|
const required: string[] = [];
|
||||||
|
|
||||||
|
if (!usage) {
|
||||||
|
// No usage string — command takes no arguments
|
||||||
|
return { properties, required };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove the command name prefix
|
||||||
|
const argsPart = usage.replace(new RegExp(`^${command}\\s*`), '').trim();
|
||||||
|
if (!argsPart) return { properties, required };
|
||||||
|
|
||||||
|
// Extract <required> params
|
||||||
|
const requiredMatches = argsPart.matchAll(/<([^>]+)>/g);
|
||||||
|
for (const match of requiredMatches) {
|
||||||
|
const name = match[1].replace(/[^a-zA-Z0-9_]/g, '_');
|
||||||
|
properties[name] = { type: 'string', description: `Required: ${match[1]}` };
|
||||||
|
required.push(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract [optional] params
|
||||||
|
const optionalMatches = argsPart.matchAll(/\[([^\]]+)\]/g);
|
||||||
|
for (const match of optionalMatches) {
|
||||||
|
const name = match[1].replace(/[^a-zA-Z0-9_]/g, '_').replace(/^-+/, '');
|
||||||
|
properties[name] = { type: 'string', description: `Optional: ${match[1]}` };
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no params extracted but there's content, add a generic args param
|
||||||
|
if (Object.keys(properties).length === 0 && argsPart.length > 0) {
|
||||||
|
properties['args'] = { type: 'string', description: `Arguments: ${argsPart}` };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { properties, required };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate MCP tool manifest from COMMAND_DESCRIPTIONS.
|
||||||
|
*/
|
||||||
|
export function generateMCPManifest(): MCPTool[] {
|
||||||
|
const tools: MCPTool[] = [];
|
||||||
|
|
||||||
|
for (const [cmd, meta] of Object.entries(COMMAND_DESCRIPTIONS)) {
|
||||||
|
const { properties, required } = parseUsageParams(meta.usage, cmd);
|
||||||
|
const isReadOnly = READ_COMMANDS.has(cmd);
|
||||||
|
|
||||||
|
tools.push({
|
||||||
|
name: `browse_${cmd}`,
|
||||||
|
description: meta.description,
|
||||||
|
inputSchema: {
|
||||||
|
type: 'object',
|
||||||
|
properties,
|
||||||
|
required,
|
||||||
|
},
|
||||||
|
annotations: {
|
||||||
|
category: meta.category,
|
||||||
|
readOnly: isReadOnly,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return tools;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── CLI ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
if (import.meta.main) {
|
||||||
|
const manifest = {
|
||||||
|
name: 'gstack-browse',
|
||||||
|
version: '1.0.0',
|
||||||
|
description: 'Headless browser automation for AI agents — navigate, interact, screenshot, inspect',
|
||||||
|
tools: generateMCPManifest(),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (process.argv.includes('--write')) {
|
||||||
|
const outPath = path.join(import.meta.dir, '..', 'mcp-tools.json');
|
||||||
|
fs.writeFileSync(outPath, JSON.stringify(manifest, null, 2) + '\n');
|
||||||
|
console.log(`Written: ${outPath} (${manifest.tools.length} tools)`);
|
||||||
|
} else {
|
||||||
|
console.log(JSON.stringify(manifest, null, 2));
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue