fix: MCP extension mode, network output parsing, URL template generation

- Add --extension flag when spawning Playwright MCP server
- Fix findMcpServerPath to find @playwright/mcp/cli.js locally
- Fix parseNetworkOutput regex to handle [GET] url => [200] format
- Add buildTemplatedUrl: strips volatile params (w_rid/wts), templates
  search/limit/pagination params without URL encoding artifacts
- Add fileURLToPath import
This commit is contained in:
jackwener committed 2026-03-15 06:41:01 +08:00
1 parent 668421db3d
commit 932ab9097f
2 files changed
+68 -31

No files matched your search

+22 -4
View File
@@ -118,17 +118,33 @@ interface InferredCapability {
/**
* Parse raw network output from Playwright MCP into structured entries.
* Handles both text format ([GET] url => [200]) and structured JSON.
*/
function parseNetworkOutput(raw: any): NetworkEntry[] {
if (typeof raw === 'string') {
// Playwright MCP returns network as text lines like:
// "GET https://api.example.com/xxx → 200 (application/json)"
// "[GET] https://api.example.com/xxx => [200] "
// May also have markdown headers like "### Result"
const entries: NetworkEntry[] = [];
const lines = raw.split('\n').filter((l: string) => l.trim());
for (const line of lines) {
const match = line.match(/^(GET|POST|PUT|DELETE|PATCH|OPTIONS)\s+(\S+)\s*→?\s*(\d+)?\s*(?:\(([^)]*)\))?/i);
if (match) {
const [, method, url, status, ct] = match;
// Format: [METHOD] URL => [STATUS] optional_extra
const bracketMatch = line.match(/^\[?(GET|POST|PUT|DELETE|PATCH|OPTIONS)\]?\s+(\S+)\s*(?:=>|→)\s*\[?(\d+)\]?/i);
if (bracketMatch) {
const [, method, url, status] = bracketMatch;
entries.push({
method: method.toUpperCase(),
url,
status: status ? parseInt(status) : null,
contentType: url.endsWith('.json') ? 'application/json' :
(url.includes('/api/') || url.includes('/x/')) ? 'application/json' : '',
});
continue;
}
// Legacy format: GET url → 200 (application/json)
const legacyMatch = line.match(/^(GET|POST|PUT|DELETE|PATCH|OPTIONS)\s+(\S+)\s*→?\s*(\d+)?\s*(?:\(([^)]*)\))?/i);
if (legacyMatch) {
const [, method, url, status, ct] = legacyMatch;
entries.push({
method: method.toUpperCase(),
url,
@@ -152,6 +168,8 @@ function parseNetworkOutput(raw: any): NetworkEntry[] {
return [];
}
/**
* Normalize a URL into a pattern by replacing IDs with placeholders.
*/
+46 -27
View File
@@ -70,6 +70,49 @@ export function synthesizeFromExplore(target: string, opts: any = {}): any {
};
}
/** Volatile params to strip from generated URLs */
const VOLATILE_PARAMS = new Set(['w_rid', 'wts', 'callback', '_', 'timestamp', 't', 'nonce', 'sign']);
const SEARCH_PARAM_NAMES = new Set(['q', 'query', 'keyword', 'search', 'wd', 'kw', 'w', 'search_query']);
const LIMIT_PARAM_NAMES = new Set(['ps', 'page_size', 'limit', 'count', 'per_page', 'size', 'num']);
const PAGE_PARAM_NAMES = new Set(['pn', 'page', 'page_num', 'offset', 'cursor']);
/**
* Build a clean templated URL from a raw API URL.
* - Strips volatile params (w_rid, wts, etc.)
* - Templates search, limit, and pagination params
* - Builds URL string manually to avoid URL encoding of ${{ }} expressions
*/
function buildTemplatedUrl(rawUrl: string, cap: any, endpoint: any): string {
try {
const u = new URL(rawUrl);
const base = `${u.protocol}//${u.host}${u.pathname}`;
const params: Array<[string, string]> = [];
const hasKeyword = cap.recommendedArgs?.some((a: any) => a.name === 'keyword');
u.searchParams.forEach((v, k) => {
// Skip volatile params
if (VOLATILE_PARAMS.has(k)) return;
// Template known param types
if (hasKeyword && SEARCH_PARAM_NAMES.has(k)) {
params.push([k, '${{ args.keyword }}']);
} else if (LIMIT_PARAM_NAMES.has(k)) {
params.push([k, '${{ args.limit | default(20) }}']);
} else if (PAGE_PARAM_NAMES.has(k)) {
params.push([k, '${{ args.page | default(1) }}']);
} else {
params.push([k, v]);
}
});
if (params.length === 0) return base;
return base + '?' + params.map(([k, v]) => `${k}=${v}`).join('&');
} catch {
return rawUrl;
}
}
/**
* Build a YAML pipeline definition from a capability + endpoint.
*/
@@ -82,33 +125,9 @@ function buildCandidateYaml(site: string, manifest: any, cap: any, endpoint: any
pipeline.push({ navigate: manifest.target_url });
}
// Step 2: Fetch the API
const fetchUrl = endpoint?.url ?? manifest.target_url;
const fetchStep: any = { url: fetchUrl };
// If it has search params, template them
if (cap.recommendedArgs?.some((a: any) => a.name === 'keyword')) {
try {
const u = new URL(fetchUrl);
// Find the search query param and template it
for (const [k, _v] of u.searchParams) {
if (['q', 'query', 'keyword', 'search', 'wd', 'kw', 'w', 'search_query'].includes(k)) {
u.searchParams.set(k, '${{ args.keyword }}');
}
}
fetchStep.url = u.toString().replace(/%24%7B%7B/g, '${{').replace(/%7D%7D/g, '}}');
} catch {}
}
// Add limit param if supported
if (endpoint?.queryParams?.some((p: string) => ['ps', 'page_size', 'limit', 'count', 'per_page', 'size', 'num'].includes(p))) {
try {
const u = new URL(fetchStep.url);
const limitParam = endpoint.queryParams.find((p: string) => ['ps', 'page_size', 'limit', 'count', 'per_page', 'size', 'num'].includes(p));
if (limitParam) u.searchParams.set(limitParam, '${{ args.limit | default(20) }}');
fetchStep.url = u.toString().replace(/%24%7B%7B/g, '${{').replace(/%7D%7D/g, '}}');
} catch {}
}
// Step 2: Fetch the API — build a clean URL with templates
const rawUrl = endpoint?.url ?? manifest.target_url;
const fetchStep: any = { url: buildTemplatedUrl(rawUrl, cap, endpoint) };
pipeline.push({ fetch: fetchStep });