@wordbricks/playwright-mcp 0.1.19 → 0.1.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/README.md +54 -44
  2. package/cli-wrapper.js +15 -14
  3. package/cli.js +1 -1
  4. package/config.d.ts +11 -6
  5. package/index.d.ts +7 -5
  6. package/index.js +1 -1
  7. package/lib/browserContextFactory.js +131 -58
  8. package/lib/browserServerBackend.js +14 -12
  9. package/lib/config.js +60 -46
  10. package/lib/context.js +41 -39
  11. package/lib/extension/cdpRelay.js +67 -61
  12. package/lib/extension/extensionContextFactory.js +10 -10
  13. package/lib/frameworkPatterns.js +21 -21
  14. package/lib/hooks/antiBotDetectionHook.js +178 -0
  15. package/lib/hooks/core.js +11 -10
  16. package/lib/hooks/eventConsumer.js +29 -16
  17. package/lib/hooks/events.js +3 -3
  18. package/lib/hooks/formatToolCallEvent.js +3 -7
  19. package/lib/hooks/frameworkStateHook.js +40 -40
  20. package/lib/hooks/grouping.js +3 -3
  21. package/lib/hooks/jsonLdDetectionHook.js +44 -37
  22. package/lib/hooks/networkFilters.js +24 -15
  23. package/lib/hooks/networkSetup.js +11 -6
  24. package/lib/hooks/networkTrackingHook.js +31 -19
  25. package/lib/hooks/pageHeightHook.js +9 -9
  26. package/lib/hooks/registry.js +18 -16
  27. package/lib/hooks/requireTabHook.js +3 -3
  28. package/lib/hooks/schema.js +44 -32
  29. package/lib/hooks/waitHook.js +7 -7
  30. package/lib/index.js +12 -10
  31. package/lib/mcp/inProcessTransport.js +3 -4
  32. package/lib/mcp/proxyBackend.js +43 -28
  33. package/lib/mcp/server.js +24 -19
  34. package/lib/mcp/tool.js +14 -8
  35. package/lib/mcp/transport.js +60 -53
  36. package/lib/playwrightTransformer.js +129 -106
  37. package/lib/program.js +54 -52
  38. package/lib/response.js +36 -30
  39. package/lib/sessionLog.js +19 -17
  40. package/lib/tab.js +41 -39
  41. package/lib/tools/common.js +19 -19
  42. package/lib/tools/console.js +11 -11
  43. package/lib/tools/dialogs.js +18 -15
  44. package/lib/tools/evaluate.js +26 -17
  45. package/lib/tools/extractFrameworkState.js +48 -37
  46. package/lib/tools/files.js +17 -14
  47. package/lib/tools/form.js +32 -23
  48. package/lib/tools/getSnapshot.js +14 -15
  49. package/lib/tools/getVisibleHtml.js +33 -17
  50. package/lib/tools/install.js +20 -20
  51. package/lib/tools/keyboard.js +29 -24
  52. package/lib/tools/mouse.js +29 -31
  53. package/lib/tools/navigate.js +19 -23
  54. package/lib/tools/network.js +12 -14
  55. package/lib/tools/networkDetail.js +68 -61
  56. package/lib/tools/networkSearch/bodySearch.js +46 -32
  57. package/lib/tools/networkSearch/grouping.js +15 -6
  58. package/lib/tools/networkSearch/helpers.js +4 -4
  59. package/lib/tools/networkSearch/searchHtml.js +25 -16
  60. package/lib/tools/networkSearch/urlSearch.js +56 -14
  61. package/lib/tools/networkSearch.js +65 -35
  62. package/lib/tools/pdf.js +13 -12
  63. package/lib/tools/repl.js +66 -54
  64. package/lib/tools/screenshot.js +57 -33
  65. package/lib/tools/scroll.js +29 -24
  66. package/lib/tools/snapshot.js +66 -49
  67. package/lib/tools/tabs.js +22 -19
  68. package/lib/tools/tool.js +5 -3
  69. package/lib/tools/utils.js +17 -13
  70. package/lib/tools/wait.js +24 -19
  71. package/lib/tools.js +21 -20
  72. package/lib/utils/adBlockFilter.js +29 -26
  73. package/lib/utils/codegen.js +20 -16
  74. package/lib/utils/extensionPath.js +4 -4
  75. package/lib/utils/fileUtils.js +17 -13
  76. package/lib/utils/graphql.js +69 -58
  77. package/lib/utils/guid.js +3 -3
  78. package/lib/utils/httpServer.js +9 -9
  79. package/lib/utils/log.js +3 -3
  80. package/lib/utils/manualPromise.js +7 -7
  81. package/lib/utils/networkFormat.js +7 -5
  82. package/lib/utils/package.js +4 -4
  83. package/lib/utils/sanitizeHtml.js +66 -34
  84. package/lib/utils/truncate.js +25 -25
  85. package/lib/utils/withTimeout.js +1 -1
  86. package/package.json +34 -57
  87. package/src/index.ts +27 -17
  88. package/LICENSE +0 -202
@@ -1,28 +1,30 @@
1
- import { withTimeout } from '../../utils/withTimeout.js';
2
- import { searchInHtml } from './searchHtml.js';
3
- import { highlightMatch } from './helpers.js';
1
+ import { withTimeout } from "../../utils/withTimeout.js";
2
+ import { highlightMatch } from "./helpers.js";
3
+ import { searchInHtml } from "./searchHtml.js";
4
4
  const CONTEXT_LENGTH_BODY = 400;
5
5
  const CONTEXT_LENGTH_DEFAULT = 300;
6
6
  const MAX_SEARCH_DEPTH = 20;
7
7
  export const searchInObject = (obj, keyword, path, matches, source, depth = 0) => {
8
- if (depth > MAX_SEARCH_DEPTH || !obj || typeof obj !== 'object')
8
+ if (depth > MAX_SEARCH_DEPTH || !obj || typeof obj !== "object")
9
9
  return;
10
10
  for (const key in obj) {
11
11
  const newPath = path ? `${path}.${key}` : key;
12
12
  const value = obj[key];
13
13
  if (key.toLowerCase().includes(keyword)) {
14
14
  const highlightedKey = highlightMatch(key, keyword, CONTEXT_LENGTH_DEFAULT);
15
- const valueContext = typeof value === 'object' && value !== null
15
+ const valueContext = typeof value === "object" && value !== null
16
16
  ? truncateJsonPreview(value)
17
17
  : truncateStringPreview(String(value));
18
18
  matches.push({
19
19
  path: newPath,
20
- value: typeof value === 'object' && value !== null ? JSON.stringify(value) : String(value),
20
+ value: typeof value === "object" && value !== null
21
+ ? JSON.stringify(value)
22
+ : String(value),
21
23
  context: `${highlightedKey}: ${valueContext}`,
22
24
  source,
23
25
  });
24
26
  }
25
- if (typeof value === 'string' && value.toLowerCase().includes(keyword)) {
27
+ if (typeof value === "string" && value.toLowerCase().includes(keyword)) {
26
28
  matches.push({
27
29
  path: newPath,
28
30
  value: value,
@@ -30,7 +32,7 @@ export const searchInObject = (obj, keyword, path, matches, source, depth = 0) =
30
32
  source,
31
33
  });
32
34
  }
33
- else if (typeof value === 'number' && String(value).includes(keyword)) {
35
+ else if (typeof value === "number" && String(value).includes(keyword)) {
34
36
  const valueStr = String(value);
35
37
  matches.push({
36
38
  path: newPath,
@@ -39,50 +41,56 @@ export const searchInObject = (obj, keyword, path, matches, source, depth = 0) =
39
41
  source,
40
42
  });
41
43
  }
42
- if (typeof value === 'object' && value !== null)
44
+ if (typeof value === "object" && value !== null)
43
45
  searchInObject(value, keyword, newPath, matches, source, depth + 1);
44
46
  }
45
47
  };
46
- const truncateStringPreview = (text) => text.length > CONTEXT_LENGTH_DEFAULT ? text.slice(0, CONTEXT_LENGTH_DEFAULT) + '...' : text;
48
+ const truncateStringPreview = (text) => text.length > CONTEXT_LENGTH_DEFAULT
49
+ ? text.slice(0, CONTEXT_LENGTH_DEFAULT) + "..."
50
+ : text;
47
51
  const truncateJsonPreview = (obj) => {
48
52
  try {
49
- return JSON.stringify(obj).slice(0, CONTEXT_LENGTH_BODY) + '...';
53
+ return JSON.stringify(obj).slice(0, CONTEXT_LENGTH_BODY) + "...";
50
54
  }
51
55
  catch {
52
- return '[Object]';
56
+ return "[Object]";
53
57
  }
54
58
  };
55
59
  export const searchInRequestBody = (request, keyword, matches) => {
56
60
  const body = request.postData();
57
61
  if (!body || !body.toLowerCase().includes(keyword))
58
62
  return;
59
- const requestContentType = request.headers()['content-type'] || '';
63
+ const requestContentType = request.headers()["content-type"] || "";
60
64
  let handled = false;
61
- if (requestContentType.includes('application/json') || body.trim().startsWith('{') || body.trim().startsWith('[')) {
65
+ if (requestContentType.includes("application/json") ||
66
+ body.trim().startsWith("{") ||
67
+ body.trim().startsWith("[")) {
62
68
  try {
63
69
  const parsed = JSON.parse(body);
64
- searchInObject(parsed, keyword, 'request.body', matches, 'requestBody');
70
+ searchInObject(parsed, keyword, "request.body", matches, "requestBody");
65
71
  handled = true;
66
72
  }
67
73
  catch { }
68
74
  }
69
- if (!handled && (requestContentType.includes('text/html') || body.trim().startsWith('<'))) {
70
- searchInHtml(body, keyword, 'request.body', matches, 'requestBody');
75
+ if (!handled &&
76
+ (requestContentType.includes("text/html") || body.trim().startsWith("<"))) {
77
+ searchInHtml(body, keyword, "request.body", matches, "requestBody");
71
78
  handled = true;
72
79
  }
73
- if (!handled && requestContentType.includes('application/x-www-form-urlencoded')) {
80
+ if (!handled &&
81
+ requestContentType.includes("application/x-www-form-urlencoded")) {
74
82
  try {
75
83
  const sp = new URLSearchParams(body);
76
84
  for (const [k, v] of sp.entries()) {
77
- const vStr = v || '';
85
+ const vStr = v || "";
78
86
  if (!vStr.toLowerCase().includes(keyword))
79
87
  continue;
80
88
  const trimmed = vStr.trim();
81
89
  let parsedOk = false;
82
- if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
90
+ if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
83
91
  try {
84
92
  const parsed = JSON.parse(trimmed);
85
- searchInObject(parsed, keyword, `request.body.form.${k}`, matches, 'requestBody');
93
+ searchInObject(parsed, keyword, `request.body.form.${k}`, matches, "requestBody");
86
94
  parsedOk = true;
87
95
  }
88
96
  catch { }
@@ -92,7 +100,7 @@ export const searchInRequestBody = (request, keyword, matches) => {
92
100
  path: `request.body.form.${k}`,
93
101
  value: vStr,
94
102
  context: `${k}=${highlightMatch(vStr, keyword, CONTEXT_LENGTH_DEFAULT)}`,
95
- source: 'requestBody',
103
+ source: "requestBody",
96
104
  });
97
105
  }
98
106
  }
@@ -102,16 +110,18 @@ export const searchInRequestBody = (request, keyword, matches) => {
102
110
  }
103
111
  if (!handled) {
104
112
  matches.push({
105
- path: 'request.body',
113
+ path: "request.body",
106
114
  value: body,
107
115
  context: highlightMatch(body, keyword, CONTEXT_LENGTH_BODY),
108
- source: 'requestBody',
116
+ source: "requestBody",
109
117
  });
110
118
  }
111
119
  };
112
120
  export const searchInResponseBody = async (response, keyword, matches) => {
113
- const contentTypeHeader = response.headers()['content-type'] || '';
114
- if (!(contentTypeHeader.startsWith('text/') || contentTypeHeader.includes('application/json') || contentTypeHeader.includes('application/xml')))
121
+ const contentTypeHeader = response.headers()["content-type"] || "";
122
+ if (!(contentTypeHeader.startsWith("text/") ||
123
+ contentTypeHeader.includes("application/json") ||
124
+ contentTypeHeader.includes("application/xml")))
115
125
  return;
116
126
  try {
117
127
  const responseBody = await withTimeout(response.text(), 2000);
@@ -122,24 +132,28 @@ export const searchInResponseBody = async (response, keyword, matches) => {
122
132
  const xssiPrefix = ")]}'";
123
133
  const hasXssi = trimmed.startsWith(xssiPrefix);
124
134
  const jsonCandidate = hasXssi ? trimmed.slice(xssiPrefix.length) : trimmed;
125
- if (contentTypeHeader.includes('application/json') || jsonCandidate.startsWith('{') || jsonCandidate.startsWith('[')) {
135
+ if (contentTypeHeader.includes("application/json") ||
136
+ jsonCandidate.startsWith("{") ||
137
+ jsonCandidate.startsWith("[")) {
126
138
  try {
127
139
  const parsed = JSON.parse(jsonCandidate);
128
- searchInObject(parsed, keyword, 'response.body', matches, 'responseBody');
140
+ searchInObject(parsed, keyword, "response.body", matches, "responseBody");
129
141
  handled = true;
130
142
  }
131
143
  catch { }
132
144
  }
133
- if (!handled && (contentTypeHeader.includes('text/html') || responseBody.trim().startsWith('<'))) {
134
- searchInHtml(responseBody, keyword, 'response.body', matches, 'responseBody');
145
+ if (!handled &&
146
+ (contentTypeHeader.includes("text/html") ||
147
+ responseBody.trim().startsWith("<"))) {
148
+ searchInHtml(responseBody, keyword, "response.body", matches, "responseBody");
135
149
  handled = true;
136
150
  }
137
151
  if (!handled) {
138
152
  matches.push({
139
- path: 'response.body',
153
+ path: "response.body",
140
154
  value: responseBody,
141
155
  context: highlightMatch(responseBody, keyword, CONTEXT_LENGTH_BODY),
142
- source: 'responseBody',
156
+ source: "responseBody",
143
157
  });
144
158
  }
145
159
  }
@@ -1,7 +1,7 @@
1
1
  export const normalizePath = (path) => {
2
- if (path.includes(' > '))
3
- return path.replace(/:nth-child\(\d+\)/g, ':nth-child(*)');
4
- return path.replace(/\.(?:\d+)(?=\.|$)/g, '.*');
2
+ if (path.includes(" > "))
3
+ return path.replace(/:nth-child\(\d+\)/g, ":nth-child(*)");
4
+ return path.replace(/\.(?:\d+)(?=\.|$)/g, ".*");
5
5
  };
6
6
  export const getDepth = (path) => {
7
7
  const separators = path.match(/\.|>/g) || [];
@@ -10,18 +10,27 @@ export const getDepth = (path) => {
10
10
  export const mergeGroupedMatches = (a, b) => {
11
11
  const map = new Map();
12
12
  for (const g of a)
13
- map.set(g.normalized, { normalized: g.normalized, count: g.count, examples: g.examples.slice(0, 3) });
13
+ map.set(g.normalized, {
14
+ normalized: g.normalized,
15
+ count: g.count,
16
+ examples: g.examples.slice(0, 3),
17
+ });
14
18
  for (const g of b) {
15
19
  const existing = map.get(g.normalized);
16
20
  if (existing) {
17
21
  existing.count += g.count;
18
22
  for (const e of g.examples) {
19
- if (existing.examples.length < 3 && !existing.examples.some(x => x.context === e.context))
23
+ if (existing.examples.length < 3 &&
24
+ !existing.examples.some((x) => x.context === e.context))
20
25
  existing.examples.push(e);
21
26
  }
22
27
  }
23
28
  else {
24
- map.set(g.normalized, { normalized: g.normalized, count: g.count, examples: g.examples.slice(0, 3) });
29
+ map.set(g.normalized, {
30
+ normalized: g.normalized,
31
+ count: g.count,
32
+ examples: g.examples.slice(0, 3),
33
+ });
25
34
  }
26
35
  }
27
36
  return Array.from(map.values());
@@ -1,4 +1,4 @@
1
- import { truncateStringTo } from '../../utils/truncate.js';
1
+ import { truncateStringTo } from "../../utils/truncate.js";
2
2
  export const parseKeywordParams = (keyword) => {
3
3
  const out = [];
4
4
  try {
@@ -20,13 +20,13 @@ export const highlightMatch = (text, keyword, maxLength = 50) => {
20
20
  const contextAfter = maxLength - keyword.length - contextBefore;
21
21
  const start = Math.max(0, index - contextBefore);
22
22
  const end = Math.min(text.length, index + keyword.length + contextAfter);
23
- let result = '';
23
+ let result = "";
24
24
  if (start > 0)
25
- result += '...';
25
+ result += "...";
26
26
  result += text.substring(start, index);
27
27
  result += text.substring(index, index + keyword.length);
28
28
  result += text.substring(index + keyword.length, end);
29
29
  if (end < text.length)
30
- result += '...';
30
+ result += "...";
31
31
  return result;
32
32
  };
@@ -1,21 +1,26 @@
1
- import * as cheerio from 'cheerio';
2
- import { highlightMatch } from './helpers.js';
1
+ import * as cheerio from "cheerio";
2
+ import { highlightMatch } from "./helpers.js";
3
3
  export const getElementPath = ($el) => {
4
4
  const path = [];
5
5
  let current = $el;
6
- while (current.length && current[0].type === 'tag' && current[0].name !== 'html') {
6
+ while (current.length &&
7
+ current[0].type === "tag" &&
8
+ current[0].name !== "html") {
7
9
  let selector = current[0].name.toLowerCase();
8
- const id = current.attr('id');
10
+ const id = current.attr("id");
9
11
  if (id) {
10
12
  selector += `#${id}`;
11
13
  path.unshift(selector);
12
14
  break;
13
15
  }
14
- const classes = current.attr('class');
16
+ const classes = current.attr("class");
15
17
  if (classes)
16
- selector += `.${classes.trim().split(/\s+/).join('.')}`;
17
- const tagName = current[0].type === 'tag' ? current[0].name : '';
18
- const siblings = current.parent().children().filter((_, el) => el.type === 'tag' && el.name === tagName);
18
+ selector += `.${classes.trim().split(/\s+/).join(".")}`;
19
+ const tagName = current[0].type === "tag" ? current[0].name : "";
20
+ const siblings = current
21
+ .parent()
22
+ .children()
23
+ .filter((_, el) => el.type === "tag" && el.name === tagName);
19
24
  if (siblings.length > 1) {
20
25
  const index = siblings.index(current) + 1;
21
26
  selector += `:nth-child(${index})`;
@@ -23,13 +28,15 @@ export const getElementPath = ($el) => {
23
28
  path.unshift(selector);
24
29
  current = current.parent();
25
30
  }
26
- return path.join(' > ');
31
+ return path.join(" > ");
27
32
  };
28
33
  export const searchInHtml = (html, keyword, basePath, matches, source) => {
29
34
  const $ = cheerio.load(html);
30
- $('*').contents().each((_, node) => {
31
- if (node.type === 'text') {
32
- const text = (node.data || '').trim();
35
+ $("*")
36
+ .contents()
37
+ .each((_, node) => {
38
+ if (node.type === "text") {
39
+ const text = (node.data || "").trim();
33
40
  if (text && text.toLowerCase().includes(keyword)) {
34
41
  const parent = $(node).parent();
35
42
  if (parent.length) {
@@ -45,14 +52,16 @@ export const searchInHtml = (html, keyword, basePath, matches, source) => {
45
52
  }
46
53
  }
47
54
  });
48
- $('*').each((_, el) => {
49
- if (el.type === 'tag') {
55
+ $("*").each((_, el) => {
56
+ if (el.type === "tag") {
50
57
  const attrs = el.attribs;
51
58
  if (attrs) {
52
59
  Object.entries(attrs).forEach(([key, val]) => {
53
- if (typeof val === 'string' && val.toLowerCase().includes(keyword)) {
60
+ if (typeof val === "string" && val.toLowerCase().includes(keyword)) {
54
61
  const elPath = getElementPath($(el));
55
- const path = elPath ? `${basePath} > ${elPath}@${key}` : `${basePath}@${key}`;
62
+ const path = elPath
63
+ ? `${basePath} > ${elPath}@${key}`
64
+ : `${basePath}@${key}`;
56
65
  matches.push({
57
66
  path,
58
67
  value: val,
@@ -1,22 +1,38 @@
1
- import { highlightMatch } from './helpers.js';
1
+ import { highlightMatch } from "./helpers.js";
2
2
  const CONTEXT_LENGTH_DEFAULT = 300;
3
3
  export const searchInUrls = (request, response, keyword, keywordParams, matches) => {
4
4
  try {
5
5
  const reqUrl = request.url();
6
6
  const reqUrlLower = reqUrl.toLowerCase();
7
- const keywordPathOnly = keyword.split('?')[0];
7
+ const keywordPathOnly = keyword.split("?")[0];
8
8
  if (reqUrlLower.includes(keyword)) {
9
- matches.push({ path: 'request.url', value: reqUrl, context: `url: ${highlightMatch(reqUrl, keyword, CONTEXT_LENGTH_DEFAULT)}`, source: 'requestUrl' });
9
+ matches.push({
10
+ path: "request.url",
11
+ value: reqUrl,
12
+ context: `url: ${highlightMatch(reqUrl, keyword, CONTEXT_LENGTH_DEFAULT)}`,
13
+ source: "requestUrl",
14
+ });
10
15
  }
11
16
  else if (keywordPathOnly && reqUrlLower.includes(keywordPathOnly)) {
12
- matches.push({ path: 'request.url', value: reqUrl, context: `url: ${highlightMatch(reqUrl, keywordPathOnly, CONTEXT_LENGTH_DEFAULT)}`, source: 'requestUrl' });
17
+ matches.push({
18
+ path: "request.url",
19
+ value: reqUrl,
20
+ context: `url: ${highlightMatch(reqUrl, keywordPathOnly, CONTEXT_LENGTH_DEFAULT)}`,
21
+ source: "requestUrl",
22
+ });
13
23
  }
14
24
  else {
15
25
  try {
16
26
  const u = new URL(reqUrl);
17
27
  const pathnameLower = u.pathname.toLowerCase();
18
- if (pathnameLower.includes(keyword) || (keywordPathOnly && pathnameLower.includes(keywordPathOnly)))
19
- matches.push({ path: 'request.url', value: reqUrl, context: `url: ${highlightMatch(reqUrl, keywordPathOnly || keyword, CONTEXT_LENGTH_DEFAULT)}`, source: 'requestUrl' });
28
+ if (pathnameLower.includes(keyword) ||
29
+ (keywordPathOnly && pathnameLower.includes(keywordPathOnly)))
30
+ matches.push({
31
+ path: "request.url",
32
+ value: reqUrl,
33
+ context: `url: ${highlightMatch(reqUrl, keywordPathOnly || keyword, CONTEXT_LENGTH_DEFAULT)}`,
34
+ source: "requestUrl",
35
+ });
20
36
  }
21
37
  catch { }
22
38
  }
@@ -29,12 +45,17 @@ export const searchInUrls = (request, response, keyword, keywordParams, matches)
29
45
  const pageVal = u.searchParams.get(name);
30
46
  if (!pageVal)
31
47
  continue;
32
- const valLower = (value || '').toLowerCase();
48
+ const valLower = (value || "").toLowerCase();
33
49
  if (!valLower)
34
50
  continue;
35
51
  const pageValLower = pageVal.toLowerCase();
36
52
  if (pageValLower.includes(valLower))
37
- matches.push({ path: `request.url.query.${name}`, value: pageVal, context: `${name}=${highlightMatch(pageVal, value, CONTEXT_LENGTH_DEFAULT)}`, source: 'requestUrl' });
53
+ matches.push({
54
+ path: `request.url.query.${name}`,
55
+ value: pageVal,
56
+ context: `${name}=${highlightMatch(pageVal, value, CONTEXT_LENGTH_DEFAULT)}`,
57
+ source: "requestUrl",
58
+ });
38
59
  }
39
60
  }
40
61
  catch { }
@@ -43,17 +64,33 @@ export const searchInUrls = (request, response, keyword, keywordParams, matches)
43
64
  const resUrl = response.url();
44
65
  const resUrlLower = resUrl.toLowerCase();
45
66
  if (resUrlLower.includes(keyword)) {
46
- matches.push({ path: 'response.url', value: resUrl, context: `url: ${highlightMatch(resUrl, keyword, CONTEXT_LENGTH_DEFAULT)}`, source: 'responseUrl' });
67
+ matches.push({
68
+ path: "response.url",
69
+ value: resUrl,
70
+ context: `url: ${highlightMatch(resUrl, keyword, CONTEXT_LENGTH_DEFAULT)}`,
71
+ source: "responseUrl",
72
+ });
47
73
  }
48
74
  else if (keywordPathOnly && resUrlLower.includes(keywordPathOnly)) {
49
- matches.push({ path: 'response.url', value: resUrl, context: `url: ${highlightMatch(resUrl, keywordPathOnly, CONTEXT_LENGTH_DEFAULT)}`, source: 'responseUrl' });
75
+ matches.push({
76
+ path: "response.url",
77
+ value: resUrl,
78
+ context: `url: ${highlightMatch(resUrl, keywordPathOnly, CONTEXT_LENGTH_DEFAULT)}`,
79
+ source: "responseUrl",
80
+ });
50
81
  }
51
82
  else {
52
83
  try {
53
84
  const u2 = new URL(resUrl);
54
85
  const pathnameLower2 = u2.pathname.toLowerCase();
55
- if (pathnameLower2.includes(keyword) || (keywordPathOnly && pathnameLower2.includes(keywordPathOnly)))
56
- matches.push({ path: 'response.url', value: resUrl, context: `url: ${highlightMatch(resUrl, keywordPathOnly || keyword, CONTEXT_LENGTH_DEFAULT)}`, source: 'responseUrl' });
86
+ if (pathnameLower2.includes(keyword) ||
87
+ (keywordPathOnly && pathnameLower2.includes(keywordPathOnly)))
88
+ matches.push({
89
+ path: "response.url",
90
+ value: resUrl,
91
+ context: `url: ${highlightMatch(resUrl, keywordPathOnly || keyword, CONTEXT_LENGTH_DEFAULT)}`,
92
+ source: "responseUrl",
93
+ });
57
94
  }
58
95
  catch { }
59
96
  }
@@ -66,12 +103,17 @@ export const searchInUrls = (request, response, keyword, keywordParams, matches)
66
103
  const pageVal = u2.searchParams.get(name);
67
104
  if (!pageVal)
68
105
  continue;
69
- const valLower = (value || '').toLowerCase();
106
+ const valLower = (value || "").toLowerCase();
70
107
  if (!valLower)
71
108
  continue;
72
109
  const pageValLower = pageVal.toLowerCase();
73
110
  if (pageValLower.includes(valLower))
74
- matches.push({ path: `response.url.query.${name}`, value: pageVal, context: `${name}=${highlightMatch(pageVal, value, CONTEXT_LENGTH_DEFAULT)}`, source: 'responseUrl' });
111
+ matches.push({
112
+ path: `response.url.query.${name}`,
113
+ value: pageVal,
114
+ context: `${name}=${highlightMatch(pageVal, value, CONTEXT_LENGTH_DEFAULT)}`,
115
+ source: "responseUrl",
116
+ });
75
117
  }
76
118
  }
77
119
  catch { }
@@ -1,24 +1,27 @@
1
- import { z } from 'zod';
2
- import { defineTool } from './tool.js';
3
- import { getNetworkEventEntry } from '../hooks/networkSetup.js';
4
- import { listNetworkEvents } from '../hooks/networkTrackingHook.js';
5
- import { normalizeUrlForGrouping } from '../hooks/networkFilters.js';
6
- import { formatNetworkSummaryLine } from '../utils/networkFormat.js';
7
- import { toJsonPathNormalized, truncateStringTo } from '../utils/truncate.js';
8
- import { searchInUrls } from './networkSearch/urlSearch.js';
9
- import { searchInRequestBody, searchInResponseBody } from './networkSearch/bodySearch.js';
10
- import { normalizePath, getDepth, mergeGroupedMatches } from './networkSearch/grouping.js';
11
- import { parseKeywordParams } from './networkSearch/helpers.js';
1
+ import { z } from "zod";
2
+ import { normalizeUrlForGrouping } from "../hooks/networkFilters.js";
3
+ import { getNetworkEventEntry } from "../hooks/networkSetup.js";
4
+ import { listNetworkEvents } from "../hooks/networkTrackingHook.js";
5
+ import { formatNetworkSummaryLine } from "../utils/networkFormat.js";
6
+ import { toJsonPathNormalized, truncateStringTo } from "../utils/truncate.js";
7
+ import { searchInRequestBody, searchInResponseBody, } from "./networkSearch/bodySearch.js";
8
+ import { getDepth, mergeGroupedMatches, normalizePath, } from "./networkSearch/grouping.js";
9
+ import { highlightMatch, parseKeywordParams } from "./networkSearch/helpers.js";
10
+ import { searchInUrls } from "./networkSearch/urlSearch.js";
11
+ import { defineTool } from "./tool.js";
12
12
  const MAX_SEARCH_RESULTS = 10;
13
13
  const MAX_GROUPS_TO_SHOW = 3;
14
14
  const networkSearchSchema = z.object({
15
- keyword: z.string().describe('Keyword or phrase; avoid generic words—specific terms reduce noise and improve precision.'),
15
+ keyword: z
16
+ .string()
17
+ .describe("Keyword or phrase; avoid generic words—specific terms reduce noise and improve precision."),
16
18
  });
17
19
  const createSourceCounts = () => ({
18
20
  requestUrl: 0,
19
21
  requestBody: 0,
20
22
  responseUrl: 0,
21
23
  responseBody: 0,
24
+ responseHeaders: 0,
22
25
  });
23
26
  const accumulateSourceCounts = (records) => {
24
27
  const counts = createSourceCounts();
@@ -31,6 +34,7 @@ const cloneSourceCounts = (counts) => ({
31
34
  requestBody: counts.requestBody,
32
35
  responseUrl: counts.responseUrl,
33
36
  responseBody: counts.responseBody,
37
+ responseHeaders: counts.responseHeaders,
34
38
  });
35
39
  const computeEventScore = (match) => {
36
40
  const ageMs = Math.max(0, Date.now() - match.timestamp);
@@ -40,22 +44,23 @@ const computeEventScore = (match) => {
40
44
  const responseBonus = match.sourceCounts.responseBody * 2;
41
45
  const requestBonus = match.sourceCounts.requestBody;
42
46
  const urlBonus = (match.sourceCounts.requestUrl + match.sourceCounts.responseUrl) * 0.25;
43
- return base + responseBonus + requestBonus + urlBonus + recencyBoost;
47
+ const headerBonus = match.sourceCounts.responseHeaders;
48
+ return (base + responseBonus + requestBonus + urlBonus + headerBonus + recencyBoost);
44
49
  };
45
50
  const formatGroupPath = (group) => {
46
51
  const normalized = group.normalized;
47
52
  const jsonPath = toJsonPathNormalized(normalized);
48
53
  if (jsonPath)
49
54
  return `json: ${jsonPath.jsonPath}`;
50
- if (!normalized.includes(' > '))
55
+ if (!normalized.includes(" > "))
51
56
  return normalized;
52
- const withoutWildcard = normalized.replace(/:nth-child\(\*\)/g, '');
53
- const trimmedPrefix = withoutWildcard.startsWith('response.body')
54
- ? withoutWildcard.slice('response.body'.length).trim()
55
- : withoutWildcard.startsWith('request.body')
56
- ? withoutWildcard.slice('request.body'.length).trim()
57
+ const withoutWildcard = normalized.replace(/:nth-child\(\*\)/g, "");
58
+ const trimmedPrefix = withoutWildcard.startsWith("response.body")
59
+ ? withoutWildcard.slice("response.body".length).trim()
60
+ : withoutWildcard.startsWith("request.body")
61
+ ? withoutWildcard.slice("request.body".length).trim()
57
62
  : withoutWildcard;
58
- const noLeadingArrow = trimmedPrefix.startsWith('>')
63
+ const noLeadingArrow = trimmedPrefix.startsWith(">")
59
64
  ? trimmedPrefix.slice(1).trim()
60
65
  : trimmedPrefix;
61
66
  return `css: ${noLeadingArrow}`;
@@ -74,21 +79,36 @@ const extractExamples = (group) => {
74
79
  }
75
80
  return items;
76
81
  };
82
+ const searchInResponseSetCookies = async (response, keyword, matches) => {
83
+ const cookies = await response.headerValues("set-cookie").catch(() => []);
84
+ if (!cookies.length)
85
+ return;
86
+ for (const [index, cookie] of cookies.entries()) {
87
+ if (!cookie.toLowerCase().includes(keyword))
88
+ continue;
89
+ matches.push({
90
+ path: `response.headers.set-cookie[${index}]`,
91
+ value: cookie,
92
+ context: highlightMatch(cookie, keyword, 120),
93
+ source: "responseHeaders",
94
+ });
95
+ }
96
+ };
77
97
  const networkSearch = defineTool({
78
- capability: 'core',
98
+ capability: "core",
79
99
  schema: {
80
- name: 'browser_network_search',
81
- title: 'Search network requests',
82
- description: 'Search for keywords in network request/response bodies and URLs',
100
+ name: "browser_network_search",
101
+ title: "Search network requests",
102
+ description: "Search for keywords in network request/response bodies and URLs",
83
103
  inputSchema: networkSearchSchema,
84
- type: 'readOnly',
104
+ type: "readOnly",
85
105
  },
86
106
  handle: async (context, params, response) => {
87
107
  await context.ensureTab();
88
108
  try {
89
109
  const keyword = params.keyword?.toLowerCase();
90
110
  if (!keyword) {
91
- response.addResult('keyword parameter is required');
111
+ response.addResult("keyword parameter is required");
92
112
  return;
93
113
  }
94
114
  const events = listNetworkEvents(context);
@@ -103,8 +123,10 @@ const networkSearch = defineTool({
103
123
  const matches = [];
104
124
  searchInUrls(request, resp, keyword, keywordParams, matches);
105
125
  searchInRequestBody(request, keyword, matches);
106
- if (resp)
126
+ if (resp) {
107
127
  await searchInResponseBody(resp, keyword, matches);
128
+ await searchInResponseSetCookies(resp, keyword, matches);
129
+ }
108
130
  if (matches.length > 0) {
109
131
  const groupMap = new Map();
110
132
  for (const m of matches) {
@@ -116,12 +138,17 @@ const networkSearch = defineTool({
116
138
  }
117
139
  group.count++;
118
140
  if (group.examples.length < 3) {
119
- const example = { context: m.context, value: m.value, originalPath: m.path !== normalized ? m.path : undefined };
120
- if (!group.examples.some(e => e.context === example.context))
141
+ const example = {
142
+ context: m.context,
143
+ value: m.value,
144
+ originalPath: m.path !== normalized ? m.path : undefined,
145
+ };
146
+ if (!group.examples.some((e) => e.context === example.context))
121
147
  group.examples.push(example);
122
148
  }
123
149
  }
124
- const groups = Array.from(groupMap.values()).sort((a, b) => b.count - a.count || getDepth(b.normalized) - getDepth(a.normalized));
150
+ const groups = Array.from(groupMap.values()).sort((a, b) => b.count - a.count ||
151
+ getDepth(b.normalized) - getDepth(a.normalized));
125
152
  const totalMatches = matches.length;
126
153
  const sourceCounts = accumulateSourceCounts(matches);
127
154
  searchMatches.push({
@@ -169,7 +196,9 @@ const networkSearch = defineTool({
169
196
  existing.groups = mergeGroupedMatches(existing.groups, m.groups);
170
197
  if (!existing.response && m.response)
171
198
  existing.response = m.response;
172
- const replacePrimary = eventScore > existing.primaryScore || (eventScore === existing.primaryScore && m.timestamp > existing.primaryTimestamp);
199
+ const replacePrimary = eventScore > existing.primaryScore ||
200
+ (eventScore === existing.primaryScore &&
201
+ m.timestamp > existing.primaryTimestamp);
173
202
  if (!replacePrimary)
174
203
  continue;
175
204
  existing.request = m.request;
@@ -191,7 +220,7 @@ const networkSearch = defineTool({
191
220
  const topMatches = aggregatedMatches.slice(0, MAX_SEARCH_RESULTS);
192
221
  const structured = {
193
222
  keyword: params.keyword,
194
- requests: topMatches.map(m => {
223
+ requests: topMatches.map((m) => {
195
224
  const method = m.request.method().toUpperCase();
196
225
  const url = m.request.url();
197
226
  // Normalize URL for grouping only; not returned in output
@@ -212,9 +241,10 @@ const networkSearch = defineTool({
212
241
  })();
213
242
  const primaryGroups = m.primaryGroups
214
243
  .slice()
215
- .sort((a, b) => b.count - a.count || getDepth(b.normalized) - getDepth(a.normalized))
244
+ .sort((a, b) => b.count - a.count ||
245
+ getDepth(b.normalized) - getDepth(a.normalized))
216
246
  .slice(0, MAX_GROUPS_TO_SHOW);
217
- const paths = primaryGroups.map(group => ({
247
+ const paths = primaryGroups.map((group) => ({
218
248
  path: formatGroupPath(group),
219
249
  examples: extractExamples(group),
220
250
  }));
@@ -236,7 +266,7 @@ const networkSearch = defineTool({
236
266
  params: queryKeys.slice(0, 6),
237
267
  paths,
238
268
  };
239
- })
269
+ }),
240
270
  };
241
271
  response.addResult(JSON.stringify(structured));
242
272
  }