Skip to content

Commit 530f5ad

Browse files
committed
feat(browse): add MCP tool manifest generator for browse commands
Generates a Model Context Protocol-compatible tool manifest from COMMAND_DESCRIPTIONS — the single source of truth. Enables any MCP-compatible agent to discover and call browse commands as tools. bun run browse/src/mcp-manifest.ts # print JSON bun run browse/src/mcp-manifest.ts --write # write mcp-tools.json Generates 53 tools (16 read-only, 37 write) with: - inputSchema derived from usage strings (<required>, [optional]) - readOnly annotation from READ_COMMANDS set - category annotation from COMMAND_DESCRIPTIONS No new dependencies. No behavior change to existing code.
1 parent 7fbf68b commit 530f5ad

1 file changed

Lines changed: 126 additions & 0 deletions

File tree

browse/src/mcp-manifest.ts

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
/**
2+
* MCP tool manifest generator for gstack browse commands.
3+
*
4+
* Generates a Model Context Protocol-compatible tool manifest from
5+
* COMMAND_DESCRIPTIONS — the single source of truth for all browse commands.
6+
*
7+
* Usage:
8+
* bun run browse/src/mcp-manifest.ts → print JSON manifest
9+
* bun run browse/src/mcp-manifest.ts --write → write to browse/mcp-tools.json
10+
*
11+
* The manifest enables any MCP-compatible agent (Claude Code, Codex, Gemini,
12+
* Cursor) to discover and call gstack browse commands as tools.
13+
*/
14+
15+
import { COMMAND_DESCRIPTIONS, READ_COMMANDS, WRITE_COMMANDS, META_COMMANDS } from './commands';
16+
import * as fs from 'fs';
17+
import * as path from 'path';
18+
19+
interface MCPToolParameter {
20+
type: string;
21+
description: string;
22+
}
23+
24+
interface MCPTool {
25+
name: string;
26+
description: string;
27+
inputSchema: {
28+
type: 'object';
29+
properties: Record<string, MCPToolParameter>;
30+
required: string[];
31+
};
32+
annotations?: {
33+
category: string;
34+
readOnly: boolean;
35+
};
36+
}
37+
38+
/**
39+
* Parse a usage string like "goto <url>" into parameter definitions.
40+
* Handles: <required>, [optional], flags like --errors.
41+
*/
42+
function parseUsageParams(usage: string | undefined, command: string): {
43+
properties: Record<string, MCPToolParameter>;
44+
required: string[];
45+
} {
46+
const properties: Record<string, MCPToolParameter> = {};
47+
const required: string[] = [];
48+
49+
if (!usage) {
50+
// No usage string — command takes no arguments
51+
return { properties, required };
52+
}
53+
54+
// Remove the command name prefix
55+
const argsPart = usage.replace(new RegExp(`^${command}\\s*`), '').trim();
56+
if (!argsPart) return { properties, required };
57+
58+
// Extract <required> params
59+
const requiredMatches = argsPart.matchAll(/<([^>]+)>/g);
60+
for (const match of requiredMatches) {
61+
const name = match[1].replace(/[^a-zA-Z0-9_]/g, '_');
62+
properties[name] = { type: 'string', description: `Required: ${match[1]}` };
63+
required.push(name);
64+
}
65+
66+
// Extract [optional] params
67+
const optionalMatches = argsPart.matchAll(/\[([^\]]+)\]/g);
68+
for (const match of optionalMatches) {
69+
const name = match[1].replace(/[^a-zA-Z0-9_]/g, '_').replace(/^-+/, '');
70+
properties[name] = { type: 'string', description: `Optional: ${match[1]}` };
71+
}
72+
73+
// If no params extracted but there's content, add a generic args param
74+
if (Object.keys(properties).length === 0 && argsPart.length > 0) {
75+
properties['args'] = { type: 'string', description: `Arguments: ${argsPart}` };
76+
}
77+
78+
return { properties, required };
79+
}
80+
81+
/**
82+
* Generate MCP tool manifest from COMMAND_DESCRIPTIONS.
83+
*/
84+
export function generateMCPManifest(): MCPTool[] {
85+
const tools: MCPTool[] = [];
86+
87+
for (const [cmd, meta] of Object.entries(COMMAND_DESCRIPTIONS)) {
88+
const { properties, required } = parseUsageParams(meta.usage, cmd);
89+
const isReadOnly = READ_COMMANDS.has(cmd);
90+
91+
tools.push({
92+
name: `browse_${cmd}`,
93+
description: meta.description,
94+
inputSchema: {
95+
type: 'object',
96+
properties,
97+
required,
98+
},
99+
annotations: {
100+
category: meta.category,
101+
readOnly: isReadOnly,
102+
},
103+
});
104+
}
105+
106+
return tools;
107+
}
108+
109+
// ─── CLI ────────────────────────────────────────────────────
110+
111+
if (import.meta.main) {
112+
const manifest = {
113+
name: 'gstack-browse',
114+
version: '1.0.0',
115+
description: 'Headless browser automation for AI agents — navigate, interact, screenshot, inspect',
116+
tools: generateMCPManifest(),
117+
};
118+
119+
if (process.argv.includes('--write')) {
120+
const outPath = path.join(import.meta.dir, '..', 'mcp-tools.json');
121+
fs.writeFileSync(outPath, JSON.stringify(manifest, null, 2) + '\n');
122+
console.log(`Written: ${outPath} (${manifest.tools.length} tools)`);
123+
} else {
124+
console.log(JSON.stringify(manifest, null, 2));
125+
}
126+
}

0 commit comments

Comments
 (0)