Setting the file. One moment. Compile · Webmcp Gen · browserbase/skills · Skills Docsasync function main
— line 165
This file
- Number
- 16.1
- Position
- 1 of 8
- Type
- JavaScript
- Size
- 6 KB
- Lines
- 187
scripts/compile.mjs
JavaScript·187 lines·6 KB
PLAYWRIGHT_STYLE_PATTERNS
=
[
8 /\bpage\.(goto|locator|click|fill|evaluate|waitFor|waitForSelector)\s*\(/,
9 /\bbrowser\.newPage\s*\(/,
10 /\bcontext\.newPage\s*\(/,
11 /\bawait\s+page\b/,
12];
13
14function usage() {
15 return "Usage: compile.mjs <artifact-dir>";
16}
17
18function assertObject(value, label) {
19 if (!value || typeof value !== "object" || Array.isArray(value)) {
20 throw new Error(`${label} must be an object.`);
21 }
22}
23
24function assertValidTool(tool) {
25 assertObject(tool, "Tool");
26 if (!TOOL_NAME_PATTERN.test(String(tool.name || ""))) {
27 throw new Error(`Invalid tool name "${tool.name}". Use 1-80 letters, numbers, underscores, or hyphens.`);
28 }
29 if (!String(tool.description || "").trim()) {
30 throw new Error(`Tool "${tool.name}" is missing a description.`);
31 }
32 assertObject(tool.inputSchema, `Tool "${tool.name}" inputSchema`);
33 assertObject(tool.implementation, `Tool "${tool.name}" implementation`);
34 if (!IMPLEMENTATION_KINDS.has(tool.implementation.kind)) {
35 throw new Error(`Tool "${tool.name}" has an invalid implementation kind.`);
36 }
37 if (!String(tool.implementation.source || "").trim()) {
38 throw new Error(`Tool "${tool.name}" is missing implementation source.`);
39 }
40}
41
42function assertValidManifest(manifest) {
43 assertObject(manifest, "Manifest");
44 if (!String(manifest.domain || "").trim()) throw new Error("Manifest is missing domain.");
45 if (!String(manifest.task || "").trim()) throw new Error("Manifest is missing task.");
46 if (!String(manifest.url || "").trim()) throw new Error("Manifest is missing url.");
47 if (!Array.isArray(manifest.tools) || manifest.tools.length === 0) {
48 throw new Error("Manifest must contain at least one tool.");
49 }
50 for (const tool of manifest.tools) assertValidTool(tool);
51}
52
53function indent(source, spaces) {
54 const prefix = " ".repeat(spaces);
55 return String(source)
56 .trim()
57 .split("\n")
58 .map((line) => `${prefix}${line}`)
59 .join("\n");
60}
61
62function serializeTool(tool) {
63 return `{
64 name: ${JSON.stringify(tool.name)},
65 description: ${JSON.stringify(tool.description)},
66 inputSchema: ${JSON.stringify(tool.inputSchema, null, 6)},
67 execute: withTimeout(async (input) => {
68${indent(tool.implementation.source, 8)}
69 }, 30000),
70 }`;
71}
72
73function emitWebMCPInitScript(manifest) {
74 assertValidManifest(manifest);
75 const tools = manifest.tools.map(serializeTool).join(",\n\n");
76
77 return `(() => {
78 if (window.self !== window.top) return;
79
80 const WEBMCP_GEN_TOOLS = [
81 ${tools}
82 ];
83
84 function normalizeError(error) {
85 if (error instanceof Error) {
86 return {
87 name: error.name,
88 message: error.message,
89 stack: error.stack,
90 };
91 }
92 return { message: String(error) };
93 }
94
95 function withTimeout(fn, timeoutMs) {
96 return async (input) => {
97 let timer;
98 try {
99 return await Promise.race([
100 fn(input || {}),
101 new Promise((_, reject) => {
102 timer = setTimeout(
103 () => reject(new Error("WebMCP tool timed out")),
104 timeoutMs,
105 );
106 }),
107 ]);
108 } catch (error) {
109 return {
110 success: false,
111 error: normalizeError(error),
112 };
113 } finally {
114 if (timer) clearTimeout(timer);
115 }
116 };
117 }
118
119 function registerTools() {
120 if (!navigator.modelContext) {
121 return {
122 success: false,
123 error: "navigator.modelContext is not available.",
124 };
125 }
126
127 for (const tool of WEBMCP_GEN_TOOLS) {
128 try {
129 navigator.modelContext.unregisterTool(tool.name);
130 } catch {
131 // Tool may not exist yet.
132 }
133 navigator.modelContext.registerTool(tool);
134 }
135
136 return {
137 success: true,
138 registeredTools: WEBMCP_GEN_TOOLS.map((tool) => tool.name),
139 };
140 }
141
142 window.__webmcpGenRegistration = registerTools();
143})();
144`;
145}
146
147function staticChecks(manifest, source) {
148 const warnings = [];
149 const errors = [];
150 if (!source.includes("navigator.modelContext.registerTool")) {
151 errors.push("Generated script does not register WebMCP tools.");
152 }
153 for (const tool of manifest.tools) {
154 const implementationSource = String(tool.implementation.source || "");
155 if (PLAYWRIGHT_STYLE_PATTERNS.some((pattern) => pattern.test(implementationSource))) {
156 warnings.push(`Tool "${tool.name}" may contain Playwright-style code; WebMCP implementations run in the page.`);
157 }
158 if (implementationSource.includes("eval(") || implementationSource.includes("new Function")) {
159 errors.push(`Tool "${tool.name}" uses eval/new Function.`);
160 }
161 }
162 return { passed: errors.length === 0, errors, warnings };
163}
164
165async function main() {
166 const artifactDir = process.argv[2];
167 if (!artifactDir) throw new Error(usage());
168
169 const resolvedArtifactDir = path.resolve(process.cwd(), artifactDir);
170 const manifestPath = path.join(resolvedArtifactDir, "manifest.json");
171 const manifest = JSON.parse(await readFile(manifestPath, "utf8"));
172 const source = emitWebMCPInitScript(manifest);
173 const checks = staticChecks(manifest, source);
174 if (!checks.passed) {
175 throw new Error(`Static checks failed:\n${checks.errors.map((error) => `- ${error}`).join("\n")}`);
176 }
177 await writeFile(path.join(resolvedArtifactDir, "webmcp.init.js"), source, "utf8");
178 console.log(`Compiled WebMCP artifact at ${resolvedArtifactDir}`);
179 if (checks.warnings.length) {
180 console.log(`Warnings:\n${checks.warnings.map((warning) => `- ${warning}`).join("\n")}`);
181 }
182}
183
184main().catch((error) => {
185 console.error(error instanceof Error ? error.message : String(error));
186 process.exitCode = 1;
187});