opera-devtools-mcp 0.2.4 → 0.3.0

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 (330) hide show
  1. package/README.md +104 -7
  2. package/build/src/McpContext.js +365 -365
  3. package/build/src/McpPage.js +475 -48
  4. package/build/src/McpResponse.js +462 -288
  5. package/build/src/SlimMcpResponse.js +1 -1
  6. package/build/src/TextSnapshot.js +101 -22
  7. package/build/src/ToolHandler.js +142 -20
  8. package/build/src/bin/check-latest-version.js +27 -1
  9. package/build/src/bin/chrome-devtools-cli-options.js +573 -10
  10. package/build/src/bin/chrome-devtools-mcp-cli-options.js +119 -32
  11. package/build/src/bin/chrome-devtools-mcp-main.js +50 -7
  12. package/build/src/bin/chrome-devtools-mcp.js +7 -4
  13. package/build/src/bin/chrome-devtools.js +57 -14
  14. package/build/src/bin/opera-devtools-cli-options.js +3 -829
  15. package/build/src/bin/opera-devtools-mcp-cli-options.js +3 -348
  16. package/build/src/bin/opera-devtools-mcp-main.js +3 -37
  17. package/build/src/bin/opera-devtools-mcp.js +4 -19
  18. package/build/src/bin/opera-devtools.js +4 -185
  19. package/build/src/browser.js +45 -5
  20. package/build/src/{PageCollector.js → collectors/PageCollector.js} +64 -133
  21. package/build/src/collectors/ServiceWorkerCollector.js +171 -0
  22. package/build/src/daemon/client.js +75 -17
  23. package/build/src/daemon/daemon.js +92 -30
  24. package/build/src/daemon/utils.js +24 -8
  25. package/build/src/{DevtoolsUtils.js → devtools/DevtoolsUtils.js} +94 -88
  26. package/build/src/devtools/McpHostBindingAdapter.js +165 -0
  27. package/build/src/{issue-descriptions.js → devtools/issueDescriptions.js} +8 -6
  28. package/build/src/formatters/ConsoleFormatter.js +44 -11
  29. package/build/src/formatters/HeapSnapshotFormatter.js +140 -9
  30. package/build/src/formatters/IssueFormatter.js +7 -7
  31. package/build/src/formatters/NetworkFormatter.js +10 -8
  32. package/build/src/index.js +101 -232
  33. package/build/src/opera/branding.js +34 -0
  34. package/build/src/opera/browserFlags.js +85 -0
  35. package/build/src/opera/browserLaunch.js +52 -0
  36. package/build/src/opera/policy.js +50 -0
  37. package/build/src/opera/serviceWorkerRetry.js +37 -0
  38. package/build/src/opera/toolHandlerHooks.js +39 -0
  39. package/build/src/{tools → opera/tools}/opera.js +9 -21
  40. package/build/src/processors/HeapSnapshotManager.js +258 -0
  41. package/build/src/{trace-processing/parse.js → processors/PerformanceTrace.js} +9 -9
  42. package/build/src/telemetry/ClearcutLogger.js +12 -119
  43. package/build/src/telemetry/WatchdogClient.js +5 -5
  44. package/build/src/telemetry/errors.js +1 -1
  45. package/build/src/telemetry/flagUtils.js +6 -6
  46. package/build/src/telemetry/{toolMetricsUtils.js → metricsRegistry.js} +4 -11
  47. package/build/src/telemetry/persistence.js +3 -3
  48. package/build/src/telemetry/transformation.js +184 -0
  49. package/build/src/telemetry/watchdog/ClearcutSender.js +11 -11
  50. package/build/src/telemetry/watchdog/main.js +6 -6
  51. package/build/src/third_party/THIRD_PARTY_NOTICES +232 -1061
  52. package/build/src/third_party/bundled-packages.json +6 -8
  53. package/build/src/third_party/devtools-formatter-worker.js +63 -22
  54. package/build/src/third_party/devtools-heap-snapshot-worker.js +809 -182
  55. package/build/src/third_party/index.js +69558 -74691
  56. package/build/src/third_party/issue-descriptions/CoepCorpNotSameOrigin.md +2 -2
  57. package/build/src/third_party/issue-descriptions/CoepCorpNotSameOriginAfterDefaultedToSameOriginByCoep.md +3 -3
  58. package/build/src/third_party/issue-descriptions/CoepCorpNotSameSite.md +2 -2
  59. package/build/src/third_party/issue-descriptions/CoepFrameResourceNeedsCoepHeader.md +1 -1
  60. package/build/src/third_party/issue-descriptions/CompatibilityModeQuirks.md +1 -1
  61. package/build/src/third_party/issue-descriptions/CookieAttributeValueExceedsMaxSize.md +2 -2
  62. package/build/src/third_party/issue-descriptions/LowTextContrast.md +1 -1
  63. package/build/src/third_party/issue-descriptions/SameSiteNoneInsecureErrorRead.md +1 -1
  64. package/build/src/third_party/issue-descriptions/SameSiteNoneInsecureErrorSet.md +1 -1
  65. package/build/src/third_party/issue-descriptions/SameSiteNoneInsecureWarnRead.md +1 -1
  66. package/build/src/third_party/issue-descriptions/SameSiteNoneInsecureWarnSet.md +1 -1
  67. package/build/src/third_party/issue-descriptions/SameSiteUnspecifiedLaxAllowUnsafeRead.md +3 -3
  68. package/build/src/third_party/issue-descriptions/SameSiteUnspecifiedLaxAllowUnsafeSet.md +3 -3
  69. package/build/src/third_party/issue-descriptions/bounceTrackingMitigations.md +1 -1
  70. package/build/src/third_party/issue-descriptions/connectionAllowlistEmbeddingRequirementNotSatisfied.md +26 -0
  71. package/build/src/third_party/issue-descriptions/connectionAllowlistIFrameAttributeLoosensEmbeddingRequirement.md +19 -0
  72. package/build/src/third_party/issue-descriptions/connectionAllowlistInvalidAllowConnectionAllowlistFrom.md +21 -0
  73. package/build/src/third_party/issue-descriptions/connectionAllowlistInvalidAllowlistItemType.md +2 -2
  74. package/build/src/third_party/issue-descriptions/connectionAllowlistInvalidHeader.md +2 -2
  75. package/build/src/third_party/issue-descriptions/connectionAllowlistInvalidUrlPattern.md +3 -3
  76. package/build/src/third_party/issue-descriptions/connectionAllowlistItemNotInnerList.md +2 -2
  77. package/build/src/third_party/issue-descriptions/connectionAllowlistMoreThanOneList.md +2 -2
  78. package/build/src/third_party/issue-descriptions/connectionAllowlistReportingEndpointNotToken.md +1 -1
  79. package/build/src/third_party/issue-descriptions/cookieCrossSiteRedirectDowngrade.md +3 -3
  80. package/build/src/third_party/issue-descriptions/cookieExcludeBlockedWithinRelatedWebsiteSet.md +1 -1
  81. package/build/src/third_party/issue-descriptions/cookieExcludePortMismatch.md +1 -1
  82. package/build/src/third_party/issue-descriptions/cookieExcludeSchemeMismatch.md +1 -1
  83. package/build/src/third_party/issue-descriptions/cookieExcludeThirdPartyPhaseoutSet.md +1 -1
  84. package/build/src/third_party/issue-descriptions/cookieWarnThirdPartyPhaseoutRead.md +1 -1
  85. package/build/src/third_party/issue-descriptions/corsAllowCredentialsRequired.md +1 -1
  86. package/build/src/third_party/issue-descriptions/corsDisabledScheme.md +2 -2
  87. package/build/src/third_party/issue-descriptions/corsDisallowedByMode.md +2 -2
  88. package/build/src/third_party/issue-descriptions/corsHeaderDisallowedByPreflightResponse.md +1 -1
  89. package/build/src/third_party/issue-descriptions/corsInvalidHeaderValues.md +3 -3
  90. package/build/src/third_party/issue-descriptions/corsLocalNetworkAccessPermissionDenied.md +1 -1
  91. package/build/src/third_party/issue-descriptions/corsMethodDisallowedByPreflightResponse.md +1 -1
  92. package/build/src/third_party/issue-descriptions/corsNoCorsRedirectModeNotFollow.md +1 -1
  93. package/build/src/third_party/issue-descriptions/corsOriginMismatch.md +3 -3
  94. package/build/src/third_party/issue-descriptions/corsPreflightResponseInvalid.md +2 -2
  95. package/build/src/third_party/issue-descriptions/corsRedirectContainsCredentials.md +3 -3
  96. package/build/src/third_party/issue-descriptions/corsWildcardOriginNotAllowed.md +4 -3
  97. package/build/src/third_party/issue-descriptions/cspEvalViolation.md +4 -4
  98. package/build/src/third_party/issue-descriptions/cspInlineViolation.md +2 -2
  99. package/build/src/third_party/issue-descriptions/cspTrustedTypesPolicyViolation.md +2 -2
  100. package/build/src/third_party/issue-descriptions/cspTrustedTypesSinkViolation.md +4 -4
  101. package/build/src/third_party/issue-descriptions/cspURLViolation.md +4 -4
  102. package/build/src/third_party/issue-descriptions/emailVerificationRequestAccountsEmptyList.md +1 -0
  103. package/build/src/third_party/issue-descriptions/emailVerificationRequestAccountsHttpNotFound.md +1 -0
  104. package/build/src/third_party/issue-descriptions/emailVerificationRequestAccountsInvalidContentType.md +1 -0
  105. package/build/src/third_party/issue-descriptions/emailVerificationRequestAccountsInvalidResponse.md +1 -0
  106. package/build/src/third_party/issue-descriptions/emailVerificationRequestAccountsNoResponse.md +1 -0
  107. package/build/src/third_party/issue-descriptions/emailVerificationRequestDnsFetchFailed.md +1 -0
  108. package/build/src/third_party/issue-descriptions/emailVerificationRequestDnsInvalidRecord.md +1 -0
  109. package/build/src/third_party/issue-descriptions/emailVerificationRequestEmailVerificationWellKnownHttpNotFound.md +1 -0
  110. package/build/src/third_party/issue-descriptions/emailVerificationRequestEmailVerificationWellKnownInvalidContentType.md +1 -0
  111. package/build/src/third_party/issue-descriptions/emailVerificationRequestEmailVerificationWellKnownInvalidResponse.md +1 -0
  112. package/build/src/third_party/issue-descriptions/emailVerificationRequestEmailVerificationWellKnownNoResponse.md +1 -0
  113. package/build/src/third_party/issue-descriptions/emailVerificationRequestInvalidEmail.md +1 -0
  114. package/build/src/third_party/issue-descriptions/emailVerificationRequestJwksHttpNotFound.md +1 -0
  115. package/build/src/third_party/issue-descriptions/emailVerificationRequestJwksInvalidResponse.md +1 -0
  116. package/build/src/third_party/issue-descriptions/emailVerificationRequestKeyBindingSigningFailed.md +1 -0
  117. package/build/src/third_party/issue-descriptions/emailVerificationRequestRpOriginIsOpaque.md +1 -0
  118. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenHttpNotFound.md +1 -0
  119. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenInvalidContentType.md +1 -0
  120. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenInvalidResponse.md +1 -0
  121. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenInvalidSdJwt.md +1 -0
  122. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenMalformedSdJwt.md +1 -0
  123. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenNoResponse.md +1 -0
  124. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationKbInvalidAudience.md +1 -0
  125. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationKbInvalidIssuedAt.md +1 -0
  126. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationKbInvalidNonce.md +1 -0
  127. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationKbInvalidSdHash.md +1 -0
  128. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationKbInvalidTyp.md +1 -0
  129. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationKbMissingAud.md +1 -0
  130. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationKbMissingCnf.md +1 -0
  131. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationKbMissingIat.md +1 -0
  132. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationKbMissingNonce.md +1 -0
  133. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationKbMissingSdHash.md +1 -0
  134. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationKbSignatureFailed.md +1 -0
  135. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationSdJwtInvalidEmail.md +1 -0
  136. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationSdJwtInvalidEmailVerified.md +1 -0
  137. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationSdJwtInvalidHolderKey.md +1 -0
  138. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationSdJwtInvalidIssuedAt.md +1 -0
  139. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationSdJwtInvalidIssuer.md +1 -0
  140. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationSdJwtJwksMissingKeys.md +1 -0
  141. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationSdJwtMissingCnf.md +1 -0
  142. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationSdJwtMissingEmail.md +1 -0
  143. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationSdJwtMissingIat.md +1 -0
  144. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationSdJwtMissingIss.md +1 -0
  145. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationSdJwtSignatureFailed.md +1 -0
  146. package/build/src/third_party/issue-descriptions/emailVerificationRequestTokenVerificationSdJwtUnsupportedHeaderAlg.md +1 -0
  147. package/build/src/third_party/issue-descriptions/emailVerificationRequestUserLoggedOut.md +1 -0
  148. package/build/src/third_party/issue-descriptions/emailVerificationRequestWellKnownAccountsEndpointCrossOrigin.md +1 -0
  149. package/build/src/third_party/issue-descriptions/emailVerificationRequestWellKnownHttpNotFound.md +1 -0
  150. package/build/src/third_party/issue-descriptions/emailVerificationRequestWellKnownInvalidContentType.md +1 -0
  151. package/build/src/third_party/issue-descriptions/emailVerificationRequestWellKnownInvalidResponse.md +1 -0
  152. package/build/src/third_party/issue-descriptions/emailVerificationRequestWellKnownIssuanceEndpointCrossOrigin.md +1 -0
  153. package/build/src/third_party/issue-descriptions/emailVerificationRequestWellKnownListEmpty.md +1 -0
  154. package/build/src/third_party/issue-descriptions/emailVerificationRequestWellKnownMissingAccountsEndpoint.md +1 -0
  155. package/build/src/third_party/issue-descriptions/emailVerificationRequestWellKnownMissingIssuanceEndpoint.md +1 -0
  156. package/build/src/third_party/issue-descriptions/emailVerificationRequestWellKnownNoResponse.md +1 -0
  157. package/build/src/third_party/issue-descriptions/emailVerificationRequestWellKnownUnsupportedSigningAlgorithm.md +1 -0
  158. package/build/src/third_party/issue-descriptions/federatedAuthRequestAccountsHttpNotFound.md +1 -1
  159. package/build/src/third_party/issue-descriptions/federatedAuthRequestAccountsInvalidResponse.md +1 -1
  160. package/build/src/third_party/issue-descriptions/federatedAuthRequestAccountsNoResponse.md +1 -1
  161. package/build/src/third_party/issue-descriptions/federatedAuthRequestApprovalDeclined.md +1 -1
  162. package/build/src/third_party/issue-descriptions/federatedAuthRequestCanceled.md +1 -1
  163. package/build/src/third_party/issue-descriptions/federatedAuthRequestErrorFetchingSignin.md +1 -1
  164. package/build/src/third_party/issue-descriptions/federatedAuthRequestErrorIdToken.md +1 -1
  165. package/build/src/third_party/issue-descriptions/federatedAuthRequestIdTokenHttpNotFound.md +1 -1
  166. package/build/src/third_party/issue-descriptions/federatedAuthRequestIdTokenInvalidRequest.md +1 -1
  167. package/build/src/third_party/issue-descriptions/federatedAuthRequestIdTokenInvalidResponse.md +1 -1
  168. package/build/src/third_party/issue-descriptions/federatedAuthRequestIdTokenNoResponse.md +1 -1
  169. package/build/src/third_party/issue-descriptions/federatedAuthRequestInvalidSigninResponse.md +1 -1
  170. package/build/src/third_party/issue-descriptions/federatedAuthRequestManifestHttpNotFound.md +1 -1
  171. package/build/src/third_party/issue-descriptions/federatedAuthRequestManifestInvalidResponse.md +1 -1
  172. package/build/src/third_party/issue-descriptions/federatedAuthRequestManifestNoResponse.md +1 -1
  173. package/build/src/third_party/issue-descriptions/federatedAuthRequestTooManyRequests.md +1 -1
  174. package/build/src/third_party/issue-descriptions/federatedAuthUserInfoRequestInvalidAccountsResponse.md +1 -1
  175. package/build/src/third_party/issue-descriptions/federatedAuthUserInfoRequestInvalidConfigOrWellKnown.md +1 -1
  176. package/build/src/third_party/issue-descriptions/federatedAuthUserInfoRequestNoAccountSharingPermission.md +1 -1
  177. package/build/src/third_party/issue-descriptions/federatedAuthUserInfoRequestNoApiPermission.md +1 -1
  178. package/build/src/third_party/issue-descriptions/federatedAuthUserInfoRequestNoReturningUserFromFetchedAccounts.md +1 -1
  179. package/build/src/third_party/issue-descriptions/federatedAuthUserInfoRequestNotIframe.md +1 -1
  180. package/build/src/third_party/issue-descriptions/federatedAuthUserInfoRequestNotPotentiallyTrustworthy.md +1 -1
  181. package/build/src/third_party/issue-descriptions/federatedAuthUserInfoRequestNotSameOrigin.md +1 -1
  182. package/build/src/third_party/issue-descriptions/federatedAuthUserInfoRequestNotSignedInWithIdp.md +1 -1
  183. package/build/src/third_party/issue-descriptions/fetchingPartitionedBlobURL.md +2 -2
  184. package/build/src/third_party/issue-descriptions/genericBackUINavigationWouldSkipAd.md +4 -0
  185. package/build/src/third_party/issue-descriptions/genericFormAriaLabelledByToNonExistingIdError.md +2 -2
  186. package/build/src/third_party/issue-descriptions/genericFormAutocompleteAttributeEmptyError.md +1 -1
  187. package/build/src/third_party/issue-descriptions/genericFormDuplicateIdForInputError.md +1 -1
  188. package/build/src/third_party/issue-descriptions/genericFormEmptyIdAndNameAttributesForInputError.md +2 -2
  189. package/build/src/third_party/issue-descriptions/genericFormInputAssignedAutocompleteValueToIdOrNameAttributeError.md +2 -2
  190. package/build/src/third_party/issue-descriptions/genericFormInputHasWrongButWellIntendedAutocompleteValueError.md +1 -1
  191. package/build/src/third_party/issue-descriptions/genericFormInputWithNoLabelError.md +1 -1
  192. package/build/src/third_party/issue-descriptions/genericFormLabelForMatchesNonExistingIdError.md +2 -2
  193. package/build/src/third_party/issue-descriptions/genericFormLabelForNameError.md +1 -1
  194. package/build/src/third_party/issue-descriptions/genericFormLabelHasNeitherForNorNestedInputError.md +1 -1
  195. package/build/src/third_party/issue-descriptions/genericFormModelContextMissingToolName.md +1 -1
  196. package/build/src/third_party/issue-descriptions/genericFormModelContextParameterMissingName.md +1 -1
  197. package/build/src/third_party/issue-descriptions/genericFormModelContextRequiredParameterMissingName.md +1 -1
  198. package/build/src/third_party/issue-descriptions/genericNavigationEntryMarkedSkippable.md +2 -2
  199. package/build/src/third_party/issue-descriptions/heavyAd.md +1 -1
  200. package/build/src/third_party/issue-descriptions/lazyLoadImageZeroSize.md +7 -0
  201. package/build/src/third_party/issue-descriptions/navigatingPartitionedBlobURL.md +3 -3
  202. package/build/src/third_party/issue-descriptions/permissionElementActivationDisabled.md +1 -1
  203. package/build/src/third_party/issue-descriptions/permissionElementActivationDisabledWithOccluder.md +1 -1
  204. package/build/src/third_party/issue-descriptions/permissionElementActivationDisabledWithOccluderParent.md +1 -1
  205. package/build/src/third_party/issue-descriptions/permissionElementFencedFrameDisallowed.md +2 -2
  206. package/build/src/third_party/issue-descriptions/permissionElementFontSizeTooLarge.md +2 -2
  207. package/build/src/third_party/issue-descriptions/permissionElementFontSizeTooSmall.md +2 -2
  208. package/build/src/third_party/issue-descriptions/permissionElementInsetBoxShadowUnsupported.md +1 -1
  209. package/build/src/third_party/issue-descriptions/permissionElementInvalidDisplayStyle.md +2 -2
  210. package/build/src/third_party/issue-descriptions/permissionElementInvalidSizeValue.md +2 -2
  211. package/build/src/third_party/issue-descriptions/permissionElementInvalidType.md +2 -2
  212. package/build/src/third_party/issue-descriptions/permissionElementInvalidTypeActivation.md +2 -2
  213. package/build/src/third_party/issue-descriptions/permissionElementNonOpaqueColor.md +1 -1
  214. package/build/src/third_party/issue-descriptions/permissionElementPaddingBottomUnsupported.md +2 -2
  215. package/build/src/third_party/issue-descriptions/permissionElementPaddingRightUnsupported.md +2 -2
  216. package/build/src/third_party/issue-descriptions/permissionElementRequestInProgress.md +2 -2
  217. package/build/src/third_party/issue-descriptions/permissionElementSecurityChecksFailed.md +1 -1
  218. package/build/src/third_party/issue-descriptions/permissionElementTypeNotSupported.md +2 -2
  219. package/build/src/third_party/issue-descriptions/permissionElementUntrustedEvent.md +1 -1
  220. package/build/src/third_party/issue-descriptions/placeholderDescriptionForInvisibleIssues.md +1 -1
  221. package/build/src/third_party/issue-descriptions/selectElementAccessibilityDisallowedOptGroupChild.md +1 -1
  222. package/build/src/third_party/issue-descriptions/selectElementAccessibilityDisallowedSelectChild.md +1 -1
  223. package/build/src/third_party/issue-descriptions/selectElementAccessibilityInteractiveContentAttributesSelectDescendant.md +1 -1
  224. package/build/src/third_party/issue-descriptions/selectElementAccessibilityInteractiveContentLegendChild.md +1 -1
  225. package/build/src/third_party/issue-descriptions/selectElementAccessibilityInteractiveContentOptionChild.md +1 -1
  226. package/build/src/third_party/issue-descriptions/selectElementAccessibilityNonPhrasingContentOptionChild.md +1 -1
  227. package/build/src/third_party/issue-descriptions/selectivePermissionsIntervention.md +3 -3
  228. package/build/src/third_party/issue-descriptions/sharedArrayBuffer.md +1 -1
  229. package/build/src/third_party/issue-descriptions/sharedDictionaryUseErrorCrossOriginNoCorsRequest.md +1 -1
  230. package/build/src/third_party/issue-descriptions/sharedDictionaryUseErrorMatchingDictionaryNotUsed.md +1 -1
  231. package/build/src/third_party/issue-descriptions/sharedDictionaryUseErrorUnexpectedContentDictionaryHeader.md +1 -1
  232. package/build/src/third_party/issue-descriptions/sharedDictionaryWriteErrorCossOriginNoCorsRequest.md +1 -1
  233. package/build/src/third_party/issue-descriptions/sharedDictionaryWriteErrorDisallowedBySettings.md +1 -1
  234. package/build/src/third_party/issue-descriptions/sharedDictionaryWriteErrorExpiredResponse.md +1 -1
  235. package/build/src/third_party/issue-descriptions/sharedDictionaryWriteErrorFeatureDisabled.md +2 -2
  236. package/build/src/third_party/issue-descriptions/sharedDictionaryWriteErrorInvalidStructuredHeader.md +1 -1
  237. package/build/src/third_party/issue-descriptions/sharedDictionaryWriteErrorNavigationRequest.md +2 -2
  238. package/build/src/third_party/issue-descriptions/sriInvalidSignatureHeader.md +4 -4
  239. package/build/src/third_party/issue-descriptions/sriInvalidSignatureInputHeader.md +4 -4
  240. package/build/src/third_party/issue-descriptions/sriMissingSignatureHeader.md +1 -1
  241. package/build/src/third_party/issue-descriptions/sriMissingSignatureInputHeader.md +2 -2
  242. package/build/src/third_party/issue-descriptions/sriSignatureHeaderValueIsIncorrectLength.md +2 -2
  243. package/build/src/third_party/issue-descriptions/sriSignatureHeaderValueIsNotByteSequence.md +4 -4
  244. package/build/src/third_party/issue-descriptions/sriSignatureHeaderValueIsParameterized.md +4 -4
  245. package/build/src/third_party/issue-descriptions/sriSignatureInputHeaderInvalidComponentName.md +1 -1
  246. package/build/src/third_party/issue-descriptions/sriSignatureInputHeaderInvalidComponentType.md +2 -2
  247. package/build/src/third_party/issue-descriptions/sriSignatureInputHeaderInvalidDerivedComponentParameter.md +1 -1
  248. package/build/src/third_party/issue-descriptions/sriSignatureInputHeaderInvalidHeaderComponentParameter.md +1 -1
  249. package/build/src/third_party/issue-descriptions/sriSignatureInputHeaderInvalidParameter.md +2 -2
  250. package/build/src/third_party/issue-descriptions/sriSignatureInputHeaderKeyIdLength.md +5 -5
  251. package/build/src/third_party/issue-descriptions/sriSignatureInputHeaderMissingLabel.md +3 -3
  252. package/build/src/third_party/issue-descriptions/sriSignatureInputHeaderMissingRequiredParameters.md +2 -2
  253. package/build/src/third_party/issue-descriptions/sriSignatureInputHeaderValueMissingComponents.md +2 -2
  254. package/build/src/third_party/issue-descriptions/sriSignatureInputHeaderValueNotInnerList.md +2 -2
  255. package/build/src/third_party/issue-descriptions/sriValidationFailedIntegrityMismatch.md +4 -4
  256. package/build/src/third_party/issue-descriptions/sriValidationFailedInvalidLength.md +3 -3
  257. package/build/src/third_party/issue-descriptions/sriValidationFailedSignatureExpired.md +2 -2
  258. package/build/src/third_party/issue-descriptions/sriValidationFailedSignatureMismatch.md +2 -2
  259. package/build/src/third_party/issue-descriptions/stylesheetLateImport.md +1 -1
  260. package/build/src/third_party/issue-descriptions/summaryElementAccessibilityInteractiveContentSummaryDescendant.md +1 -1
  261. package/build/src/third_party/issue-descriptions/unencodedDigestIncorrectDigestLength.md +2 -2
  262. package/build/src/third_party/issue-descriptions/unencodedDigestIncorrectDigestType.md +3 -3
  263. package/build/src/third_party/issue-descriptions/unencodedDigestMalformedDictionary.md +2 -2
  264. package/build/src/third_party/issue-descriptions/unencodedDigestUnknownAlgorithm.md +1 -1
  265. package/build/src/third_party/lighthouse-devtools-mcp-bundle.js +3177 -6340
  266. package/build/src/tools/ToolDefinition.js +2 -2
  267. package/build/src/tools/categories.js +3 -0
  268. package/build/src/tools/console.js +13 -0
  269. package/build/src/tools/emulation.js +30 -3
  270. package/build/src/tools/extensions.js +8 -1
  271. package/build/src/tools/input.js +246 -128
  272. package/build/src/tools/lighthouse.js +14 -7
  273. package/build/src/tools/memory.js +289 -25
  274. package/build/src/tools/network.js +10 -7
  275. package/build/src/tools/pages.js +33 -92
  276. package/build/src/tools/performance.js +67 -52
  277. package/build/src/tools/pwa.js +128 -0
  278. package/build/src/tools/screencast.js +35 -12
  279. package/build/src/tools/screenshot.js +234 -75
  280. package/build/src/tools/script.js +130 -45
  281. package/build/src/tools/slim/tools.js +3 -0
  282. package/build/src/tools/snapshot.js +7 -4
  283. package/build/src/tools/thirdPartyDeveloper.js +19 -9
  284. package/build/src/tools/tools.js +3 -1
  285. package/build/src/tools/webmcp.js +3 -1
  286. package/build/src/utils/WaitForHelper.js +299 -0
  287. package/build/src/utils/check-for-updates.js +8 -2
  288. package/build/src/utils/files.js +42 -3
  289. package/build/src/utils/logger.js +51 -0
  290. package/build/src/utils/polyfill.js +11 -0
  291. package/build/src/utils/url.js +43 -0
  292. package/build/src/version.js +1 -1
  293. package/package.json +42 -33
  294. package/build/src/DevToolsConnectionAdapter.js +0 -70
  295. package/build/src/HeapSnapshotManager.js +0 -110
  296. package/build/src/Mutex.js +0 -38
  297. package/build/src/WaitForHelper.js +0 -195
  298. package/build/src/bin/cliDefinitions.js +0 -616
  299. package/build/src/logger.js +0 -37
  300. package/build/src/polyfill.js +0 -8
  301. package/build/src/telemetry/metricUtils.js +0 -15
  302. package/build/src/third_party/issue-descriptions/SameSiteExcludeContextDowngradeRead.md +0 -8
  303. package/build/src/third_party/issue-descriptions/SameSiteExcludeContextDowngradeSet.md +0 -8
  304. package/build/src/third_party/issue-descriptions/SameSiteExcludeNavigationContextDowngrade.md +0 -8
  305. package/build/src/third_party/issue-descriptions/SameSiteWarnCrossDowngradeRead.md +0 -8
  306. package/build/src/third_party/issue-descriptions/SameSiteWarnCrossDowngradeSet.md +0 -8
  307. package/build/src/third_party/issue-descriptions/SameSiteWarnStrictLaxDowngradeStrict.md +0 -8
  308. package/build/src/third_party/issue-descriptions/arInsecureContext.md +0 -7
  309. package/build/src/third_party/issue-descriptions/arInvalidInfoHeader.md +0 -5
  310. package/build/src/third_party/issue-descriptions/arInvalidRegisterOsSourceHeader.md +0 -5
  311. package/build/src/third_party/issue-descriptions/arInvalidRegisterOsTriggerHeader.md +0 -5
  312. package/build/src/third_party/issue-descriptions/arInvalidRegisterSourceHeader.md +0 -5
  313. package/build/src/third_party/issue-descriptions/arInvalidRegisterTriggerHeader.md +0 -5
  314. package/build/src/third_party/issue-descriptions/arNavigationRegistrationUniqueScopeAlreadySet.md +0 -5
  315. package/build/src/third_party/issue-descriptions/arNavigationRegistrationWithoutTransientUserActivation.md +0 -6
  316. package/build/src/third_party/issue-descriptions/arNoRegisterOsSourceHeader.md +0 -5
  317. package/build/src/third_party/issue-descriptions/arNoRegisterOsTriggerHeader.md +0 -5
  318. package/build/src/third_party/issue-descriptions/arNoRegisterSourceHeader.md +0 -5
  319. package/build/src/third_party/issue-descriptions/arNoRegisterTriggerHeader.md +0 -5
  320. package/build/src/third_party/issue-descriptions/arNoWebOrOsSupport.md +0 -4
  321. package/build/src/third_party/issue-descriptions/arOsSourceIgnored.md +0 -18
  322. package/build/src/third_party/issue-descriptions/arOsTriggerIgnored.md +0 -19
  323. package/build/src/third_party/issue-descriptions/arPermissionPolicyDisabled.md +0 -8
  324. package/build/src/third_party/issue-descriptions/arSourceAndTriggerHeaders.md +0 -9
  325. package/build/src/third_party/issue-descriptions/arSourceIgnored.md +0 -13
  326. package/build/src/third_party/issue-descriptions/arTriggerIgnored.md +0 -12
  327. package/build/src/third_party/issue-descriptions/arUntrustworthyReportingOrigin.md +0 -10
  328. package/build/src/third_party/issue-descriptions/arWebAndOsHeaders.md +0 -11
  329. package/build/src/utils/string.js +0 -37
  330. package/build/src/utils/types.js +0 -7
@@ -7,17 +7,19 @@ import fs from 'node:fs/promises';
7
7
  import os from 'node:os';
8
8
  import path from 'node:path';
9
9
  import { fileURLToPath, pathToFileURL } from 'node:url';
10
- import { UniverseManager } from './DevtoolsUtils.js';
11
- import { HeapSnapshotManager } from './HeapSnapshotManager.js';
10
+ import { overrideDevToolsGlobals } from './devtools/DevtoolsUtils.js';
11
+ import { HeapSnapshotManager } from './processors/HeapSnapshotManager.js';
12
12
  import { McpPage } from './McpPage.js';
13
- import { NetworkCollector, ConsoleCollector, } from './PageCollector.js';
14
- import { Locator, PredefinedNetworkConditions, } from './third_party/index.js';
13
+ import { ServiceWorkerConsoleCollector } from './collectors/ServiceWorkerCollector.js';
14
+ import { Locator, } from './third_party/index.js';
15
15
  import { listPages } from './tools/pages.js';
16
16
  import { CLOSE_PAGE_ERROR } from './tools/ToolDefinition.js';
17
- import { ensureExtension, getTempFilePath } from './utils/files.js';
18
- import { getNetworkMultiplierFromString } from './WaitForHelper.js';
19
- const DEFAULT_TIMEOUT = 5_000;
20
- const NAVIGATION_TIMEOUT = 10_000;
17
+ import { getTempFilePath, resolveCanonicalPath } from './utils/files.js';
18
+ // Page ids are handed out from a process-wide counter so they stay unique
19
+ // across all contexts, in particular across browser reconnects. An id issued
20
+ // before a reconnect then fails to resolve instead of hitting an unrelated
21
+ // page of the reconnected browser.
22
+ let nextPageId = 1;
21
23
  export class McpContext {
22
24
  browser;
23
25
  logger;
@@ -25,16 +27,14 @@ export class McpContext {
25
27
  #isolatedContexts = new Map();
26
28
  // Auto-generated name counter for when no name is provided.
27
29
  #nextIsolatedContextId = 1;
28
- #pages = [];
29
30
  #extensionServiceWorkers = [];
30
31
  #mcpPages = new Map();
31
32
  #selectedPage;
32
- #networkCollector;
33
- #consoleCollector;
34
- #devtoolsUniverseManager;
33
+ #selectedPageFallback;
34
+ #serviceWorkerConsoleCollector;
35
35
  #isRunningTrace = false;
36
36
  #screenRecorderData = null;
37
- #nextPageId = 1;
37
+ #reconnectNotice = false;
38
38
  #extensionPages = new WeakMap();
39
39
  #extensionServiceWorkerMap = new WeakMap();
40
40
  #nextExtensionServiceWorkerId = 1;
@@ -43,38 +43,33 @@ export class McpContext {
43
43
  #options;
44
44
  #heapSnapshotManager = new HeapSnapshotManager();
45
45
  #roots = undefined;
46
+ #allowUnrestrictedPaths;
46
47
  constructor(browser, logger, options, locatorClass) {
48
+ overrideDevToolsGlobals({
49
+ loadResource: (url) => {
50
+ return this.loadResource(url);
51
+ },
52
+ });
47
53
  this.browser = browser;
48
54
  this.logger = logger;
49
55
  this.#locatorClass = locatorClass;
50
56
  this.#options = options;
51
- this.#networkCollector = new NetworkCollector(this.browser);
52
- this.#consoleCollector = new ConsoleCollector(this.browser, collect => {
53
- return {
54
- console: event => {
55
- collect(event);
56
- },
57
- uncaughtError: event => {
58
- collect(event);
59
- },
60
- devtoolsAggregatedIssue: event => {
61
- collect(event);
62
- },
63
- };
64
- });
65
- this.#devtoolsUniverseManager = new UniverseManager(this.browser);
57
+ this.#allowUnrestrictedPaths = options.allowUnrestrictedPaths ?? false;
58
+ this.#reconnectNotice = options.reconnected ?? false;
59
+ this.#serviceWorkerConsoleCollector = new ServiceWorkerConsoleCollector(this.browser);
66
60
  }
67
61
  async #init() {
68
- const pages = await this.createPagesSnapshot();
69
- await this.createExtensionServiceWorkersSnapshot();
70
- await this.#networkCollector.init(pages);
71
- await this.#consoleCollector.init(pages);
72
- await this.#devtoolsUniverseManager.init(pages);
62
+ await this.createPagesSnapshot();
63
+ const workers = await this.createExtensionServiceWorkersSnapshot();
64
+ await this.#serviceWorkerConsoleCollector.init(workers);
65
+ this.browser.on('targetcreated', this.#onTargetCreated);
66
+ this.browser.on('targetdestroyed', this.#onTargetDestroyed);
73
67
  }
74
68
  dispose() {
75
- this.#networkCollector.dispose();
76
- this.#consoleCollector.dispose();
77
- this.#devtoolsUniverseManager.dispose();
69
+ this.browser.off('targetcreated', this.#onTargetCreated);
70
+ this.browser.off('targetdestroyed', this.#onTargetDestroyed);
71
+ this.#serviceWorkerConsoleCollector.dispose();
72
+ this.#heapSnapshotManager.dispose();
78
73
  for (const mcpPage of this.#mcpPages.values()) {
79
74
  mcpPage.dispose();
80
75
  }
@@ -84,6 +79,40 @@ export class McpContext {
84
79
  // without destroying browser state.
85
80
  this.#isolatedContexts.clear();
86
81
  }
82
+ #onTargetCreated = async (target) => {
83
+ try {
84
+ const page = await target.page();
85
+ if (!page) {
86
+ return;
87
+ }
88
+ void this.#createMcpPage(page);
89
+ }
90
+ catch (err) {
91
+ this.logger?.('Error handling targetcreated', err);
92
+ }
93
+ };
94
+ #onTargetDestroyed = (target) => {
95
+ try {
96
+ let foundPage;
97
+ for (const page of this.#mcpPages.keys()) {
98
+ if (page.target() === target) {
99
+ foundPage = page;
100
+ break;
101
+ }
102
+ }
103
+ if (!foundPage) {
104
+ return;
105
+ }
106
+ const mcpPage = this.#mcpPages.get(foundPage);
107
+ if (mcpPage) {
108
+ mcpPage.dispose();
109
+ this.#mcpPages.delete(foundPage);
110
+ }
111
+ }
112
+ catch (err) {
113
+ this.logger?.('Error handling targetdestroyed', err);
114
+ }
115
+ };
87
116
  static async from(browser, logger, opts,
88
117
  /* Let tests use unbundled Locator class to avoid overly strict checks within puppeteer that fail when mixing bundled and unbundled class instances */
89
118
  locatorClass = Locator) {
@@ -91,12 +120,12 @@ export class McpContext {
91
120
  await context.#init();
92
121
  return context;
93
122
  }
123
+ static resetPageIdsForTesting() {
124
+ nextPageId = 1;
125
+ }
94
126
  roots() {
95
- if (this.#roots === undefined) {
96
- return undefined;
97
- }
98
127
  return [
99
- ...this.#roots,
128
+ ...(this.#roots ?? []),
100
129
  {
101
130
  uri: pathToFileURL(os.tmpdir()).href,
102
131
  name: 'temp',
@@ -106,53 +135,64 @@ export class McpContext {
106
135
  setRoots(roots) {
107
136
  this.#roots = roots;
108
137
  }
109
- validatePath(filePath) {
138
+ async validatePath(filePath) {
110
139
  if (filePath === undefined) {
111
140
  return;
112
141
  }
113
- const roots = this.roots();
114
- if (roots === undefined) {
142
+ // If the client never negotiated roots and the operator has explicitly
143
+ // opted into unrestricted access via --allow-unrestricted-paths, restore
144
+ // the previous permissive behavior and skip validation.
145
+ if (this.#roots === undefined && this.#allowUnrestrictedPaths) {
115
146
  return;
116
147
  }
117
- const absolutePath = path.resolve(filePath);
118
- for (const root of roots) {
119
- const rootPath = path.resolve(fileURLToPath(root.uri));
120
- if (absolutePath === rootPath ||
121
- absolutePath.startsWith(rootPath + path.sep)) {
122
- return;
148
+ // roots() always returns at least the temp directory, even if the
149
+ // connecting client never negotiated the optional `roots` capability.
150
+ // Path validation must not be skipped just because no workspace roots
151
+ // were configured.
152
+ const roots = this.roots();
153
+ let canonicalPath;
154
+ try {
155
+ canonicalPath = await resolveCanonicalPath(filePath);
156
+ }
157
+ catch (err) {
158
+ const errMsg = err instanceof Error ? err.message : String(err);
159
+ console.error(`[MCP Context] Error resolving real path for ${filePath}: ${errMsg}`);
160
+ throw new Error(`Access denied: Cannot resolve base path for ${filePath}.`);
161
+ }
162
+ let allowed = false;
163
+ const resolvedRoots = await Promise.allSettled(roots.map(async (root) => {
164
+ const rootPathUri = root.uri;
165
+ const rootPath = path.resolve(fileURLToPath(rootPathUri));
166
+ return await fs.realpath(rootPath);
167
+ }));
168
+ for (let i = 0; i < roots.length; i++) {
169
+ const root = roots[i];
170
+ const result = resolvedRoots[i];
171
+ if (result.status === 'fulfilled') {
172
+ const canonicalRoot = result.value;
173
+ if (canonicalPath === canonicalRoot ||
174
+ canonicalPath.startsWith(canonicalRoot + path.sep)) {
175
+ allowed = true;
176
+ break;
177
+ }
178
+ }
179
+ else {
180
+ const rootErr = result.reason;
181
+ const errMsg = rootErr instanceof Error ? rootErr.message : String(rootErr);
182
+ console.warn(`[MCP Context] Could not resolve configured root ${root.uri}: ${errMsg}`);
183
+ // Skip this root if it cannot be resolved.
123
184
  }
124
185
  }
125
- throw new Error(`Access denied: path ${filePath} is not within any of the workspace roots ${JSON.stringify(roots)}.`);
126
- }
127
- resolveCdpRequestId(page, cdpRequestId) {
128
- if (!cdpRequestId) {
129
- this.logger('no network request');
130
- return;
186
+ if (!allowed) {
187
+ throw new Error(`Access denied: path ${filePath} (canonical: ${canonicalPath}) is not within any of the configured workspace roots.`);
131
188
  }
132
- const request = this.#networkCollector.find(page.pptrPage, request => {
133
- // @ts-expect-error id is internal.
134
- return request.id === cdpRequestId;
135
- });
136
- if (!request) {
137
- this.logger('no network request for ' + cdpRequestId);
138
- return;
139
- }
140
- return this.#networkCollector.getIdForResource(request);
141
- }
142
- getNetworkRequests(page, includePreservedRequests) {
143
- return this.#networkCollector.getData(page.pptrPage, includePreservedRequests);
144
- }
145
- getConsoleData(page, includePreservedMessages) {
146
- return this.#consoleCollector.getData(page.pptrPage, includePreservedMessages);
147
- }
148
- getDevToolsUniverse(page) {
149
- return this.#devtoolsUniverseManager.get(page.pptrPage);
150
- }
151
- getConsoleMessageStableId(message) {
152
- return this.#consoleCollector.getIdForResource(message);
153
189
  }
154
- getConsoleMessageById(page, id) {
155
- return this.#consoleCollector.getById(page.pptrPage, id);
190
+ async ensureExtension(filePath, extension) {
191
+ const resolvedPath = path.resolve(filePath);
192
+ const currentExtension = path.extname(resolvedPath);
193
+ const outputPath = `${resolvedPath.slice(0, resolvedPath.length - currentExtension.length)}${extension}`;
194
+ await this.validatePath(outputPath);
195
+ return outputPath;
156
196
  }
157
197
  async newPage(background, isolatedContextName) {
158
198
  let page;
@@ -167,14 +207,13 @@ export class McpContext {
167
207
  else {
168
208
  page = await this.browser.newPage({ background });
169
209
  }
210
+ const mcpPage = await this.#createMcpPage(page);
170
211
  await this.createPagesSnapshot();
171
- this.selectPage(this.#getMcpPage(page));
172
- this.#networkCollector.addPage(page);
173
- this.#consoleCollector.addPage(page);
174
- return this.#getMcpPage(page);
212
+ this.selectPage(mcpPage);
213
+ return mcpPage;
175
214
  }
176
215
  async closePage(pageId) {
177
- if (this.#pages.length === 1) {
216
+ if (this.#mcpPages.size === 1) {
178
217
  throw new Error(CLOSE_PAGE_ERROR);
179
218
  }
180
219
  const page = this.getPageById(pageId);
@@ -184,90 +223,20 @@ export class McpContext {
184
223
  }
185
224
  await page.pptrPage.close({ runBeforeUnload: false });
186
225
  }
187
- getNetworkRequestById(page, reqid) {
188
- return this.#networkCollector.getById(page.pptrPage, reqid);
189
- }
190
- async restoreEmulation(page) {
191
- const currentSetting = page.emulationSettings;
192
- await this.emulate(currentSetting, page.pptrPage);
193
- }
194
- async emulate(options, targetPage) {
195
- const page = targetPage ?? this.getSelectedPptrPage();
196
- const mcpPage = this.#getMcpPage(page);
197
- const newSettings = { ...mcpPage.emulationSettings };
198
- if (!options.networkConditions) {
199
- await page.emulateNetworkConditions(null);
200
- delete newSettings.networkConditions;
201
- }
202
- else if (options.networkConditions === 'Offline') {
203
- await page.emulateNetworkConditions({
204
- offline: true,
205
- download: 0,
206
- upload: 0,
207
- latency: 0,
208
- });
209
- newSettings.networkConditions = 'Offline';
210
- }
211
- else if (options.networkConditions in PredefinedNetworkConditions) {
212
- const networkCondition = PredefinedNetworkConditions[options.networkConditions];
213
- await page.emulateNetworkConditions(networkCondition);
214
- newSettings.networkConditions = options.networkConditions;
215
- }
216
- if (!options.cpuThrottlingRate) {
217
- await page.emulateCPUThrottling(1);
218
- delete newSettings.cpuThrottlingRate;
219
- }
220
- else {
221
- await page.emulateCPUThrottling(options.cpuThrottlingRate);
222
- newSettings.cpuThrottlingRate = options.cpuThrottlingRate;
223
- }
224
- if (!options.geolocation) {
225
- await page.setGeolocation({ latitude: 0, longitude: 0 });
226
- delete newSettings.geolocation;
227
- }
228
- else {
229
- await page.setGeolocation(options.geolocation);
230
- newSettings.geolocation = options.geolocation;
231
- }
232
- if (!options.userAgent) {
233
- await page.setUserAgent({ userAgent: undefined });
234
- delete newSettings.userAgent;
235
- }
236
- else {
237
- await page.setUserAgent({ userAgent: options.userAgent });
238
- newSettings.userAgent = options.userAgent;
239
- }
240
- if (!options.colorScheme || options.colorScheme === 'auto') {
241
- await page.emulateMediaFeatures([
242
- { name: 'prefers-color-scheme', value: '' },
243
- ]);
244
- delete newSettings.colorScheme;
245
- }
246
- else {
247
- await page.emulateMediaFeatures([
248
- { name: 'prefers-color-scheme', value: options.colorScheme },
249
- ]);
250
- newSettings.colorScheme = options.colorScheme;
251
- }
252
- if (!options.viewport) {
253
- await page.setViewport(null);
254
- delete newSettings.viewport;
255
- }
256
- else {
257
- const defaults = {
258
- deviceScaleFactor: 1,
259
- isMobile: false,
260
- hasTouch: false,
261
- isLandscape: false,
262
- };
263
- const viewport = { ...defaults, ...options.viewport };
264
- await page.setViewport(viewport);
265
- newSettings.viewport = viewport;
266
- }
267
- mcpPage.emulationSettings = Object.keys(newSettings).length
268
- ? newSettings
269
- : {};
270
- this.#updateSelectedPageTimeouts();
226
+ get #hasNetworkBlockOrAllowlist() {
227
+ return !!(this.#options.allowList || this.#options.blocklist);
228
+ }
229
+ installPWA(options) {
230
+ return this.browser.installPWA(options);
231
+ }
232
+ uninstallPWA(options) {
233
+ return this.browser.uninstallPWA(options);
234
+ }
235
+ launchPWA(options) {
236
+ return this.browser.launchPWA(options);
237
+ }
238
+ getPWAState(options) {
239
+ return this.browser.getPWAState(options);
271
240
  }
272
241
  setIsRunningPerformanceTrace(x) {
273
242
  this.#isRunningTrace = x;
@@ -284,7 +253,10 @@ export class McpContext {
284
253
  isCruxEnabled() {
285
254
  return this.#options.performanceCrux;
286
255
  }
287
- getSelectedPptrPage() {
256
+ getPages() {
257
+ return Array.from(this.#mcpPages.values());
258
+ }
259
+ getSelectedMcpPage() {
288
260
  const page = this.#selectedPage;
289
261
  if (!page) {
290
262
  throw new Error('No page selected');
@@ -292,11 +264,37 @@ export class McpContext {
292
264
  if (page.pptrPage.isClosed()) {
293
265
  throw new Error(`The selected page has been closed. Call ${listPages().name} to see open pages.`);
294
266
  }
295
- return page.pptrPage;
267
+ return page;
296
268
  }
297
- getSelectedMcpPage() {
298
- const page = this.getSelectedPptrPage();
299
- return this.#getMcpPage(page);
269
+ async getDevToolsData(page) {
270
+ const targetPage = page ?? this.#selectedPage;
271
+ if (!targetPage) {
272
+ return undefined;
273
+ }
274
+ let timeoutId;
275
+ const timeoutPromise = new Promise(resolve => {
276
+ timeoutId = setTimeout(() => resolve(undefined), 500);
277
+ });
278
+ const dataPromise = targetPage.getDevToolsData();
279
+ try {
280
+ return await Promise.race([dataPromise, timeoutPromise]);
281
+ }
282
+ catch {
283
+ return undefined;
284
+ }
285
+ finally {
286
+ clearTimeout(timeoutId);
287
+ }
288
+ }
289
+ /**
290
+ * Returns true once if this context was created by reconnecting after the
291
+ * previous browser connection was lost, so the next response can surface a
292
+ * note. Cleared on first call.
293
+ */
294
+ consumeReconnectNotice() {
295
+ const notice = this.#reconnectNotice;
296
+ this.#reconnectNotice = false;
297
+ return notice;
300
298
  }
301
299
  getPageById(pageId) {
302
300
  const page = this.#mcpPages.values().find(mcpPage => mcpPage.id === pageId);
@@ -305,55 +303,27 @@ export class McpContext {
305
303
  }
306
304
  return page;
307
305
  }
308
- getPageId(page) {
309
- return this.#mcpPages.get(page)?.id;
310
- }
311
- #getMcpPage(page) {
312
- const mcpPage = this.#mcpPages.get(page);
313
- if (!mcpPage) {
314
- throw new Error('No McpPage found for the given page.');
315
- }
316
- return mcpPage;
317
- }
318
- #getSelectedMcpPage() {
319
- return this.#getMcpPage(this.getSelectedPptrPage());
320
- }
321
306
  isPageSelected(page) {
322
- return this.#selectedPage?.pptrPage === page;
307
+ return this.#selectedPage === page;
323
308
  }
324
309
  selectPage(newPage) {
325
310
  this.#selectedPage = newPage;
326
- this.#updateSelectedPageTimeouts();
327
- }
328
- #updateSelectedPageTimeouts() {
329
- const page = this.#getSelectedMcpPage();
330
- // For waiters 5sec timeout should be sufficient.
331
- // Increased in case we throttle the CPU
332
- const cpuMultiplier = page.cpuThrottlingRate;
333
- page.pptrPage.setDefaultTimeout(DEFAULT_TIMEOUT * cpuMultiplier);
334
- // 10sec should be enough for the load event to be emitted during
335
- // navigations.
336
- // Increased in case we throttle the network requests
337
- const networkMultiplier = getNetworkMultiplierFromString(page.networkConditions);
338
- page.pptrPage.setDefaultNavigationTimeout(NAVIGATION_TIMEOUT * networkMultiplier);
339
- }
340
- // Linear scan over per-page snapshots. The page count is small (typically
341
- // 2-10) so a reverse index isn't worthwhile given the uid-reuse lifecycle
342
- // complexity it would introduce.
343
- getAXNodeByUid(uid) {
344
- for (const mcpPage of this.#mcpPages.values()) {
345
- const node = mcpPage.textSnapshot?.idToNode.get(uid);
346
- if (node) {
347
- return node;
348
- }
349
- }
350
- return undefined;
311
+ newPage.updateTimeouts();
312
+ }
313
+ /**
314
+ * Returns details about the last page snapshot automatically replacing the
315
+ * selection because the selected page disappeared from the page list, or
316
+ * `undefined` if the snapshot left the selection intact. Recomputed on every
317
+ * createPagesSnapshot() call.
318
+ */
319
+ getSelectedPageFallback() {
320
+ return this.#selectedPageFallback;
351
321
  }
352
322
  /**
353
323
  * Creates a snapshot of the extension service workers.
354
324
  */
355
325
  async createExtensionServiceWorkersSnapshot() {
356
- const allTargets = await this.browser.targets();
326
+ const allTargets = this.browser.targets();
357
327
  const serviceWorkers = allTargets.filter(target => {
358
328
  return (target.type() === 'service_worker' &&
359
329
  target.url().includes('chrome-extension://'));
@@ -372,74 +342,16 @@ export class McpContext {
372
342
  });
373
343
  return this.#extensionServiceWorkers;
374
344
  }
375
- async createPagesSnapshot() {
376
- const { pages: allPages, isolatedContextNames } = await this.#getAllPages();
377
- for (const page of allPages) {
378
- let mcpPage = this.#mcpPages.get(page);
379
- if (!mcpPage) {
380
- mcpPage = new McpPage(page, this.#nextPageId++);
381
- this.#mcpPages.set(page, mcpPage);
382
- // We emulate a focused page for all pages to support multi-agent workflows.
383
- void page.emulateFocusedPage(true).catch(error => {
384
- this.logger('Error turning on focused page emulation', error);
385
- });
386
- }
387
- mcpPage.isolatedContextName = isolatedContextNames.get(page);
388
- }
389
- // Prune orphaned #mcpPages entries (pages that no longer exist).
390
- const currentPages = new Set(allPages);
391
- for (const [page, mcpPage] of this.#mcpPages) {
392
- if (!currentPages.has(page)) {
393
- mcpPage.dispose();
394
- this.#mcpPages.delete(page);
395
- }
396
- }
397
- this.#pages = allPages.filter(page => {
398
- return (this.#options.experimentalDevToolsDebugging ||
399
- !page.url().startsWith('devtools://'));
400
- });
401
- if ((!this.#selectedPage ||
402
- this.#pages.indexOf(this.#selectedPage.pptrPage) === -1) &&
403
- this.#pages[0]) {
404
- this.selectPage(this.#getMcpPage(this.#pages[0]));
405
- }
406
- await this.detectOpenDevToolsWindows();
407
- return this.#pages;
345
+ getServiceWorkerConsoleData(extensionId) {
346
+ return this.#serviceWorkerConsoleCollector.getData(extensionId);
408
347
  }
409
- async #getAllPages() {
410
- const defaultCtx = this.browser.defaultBrowserContext();
411
- const allPages = await this.browser.pages(this.#options.experimentalIncludeAllPages);
412
- const allTargets = this.browser.targets();
413
- const extensionTargets = allTargets.filter(target => {
414
- return (target.url().startsWith('chrome-extension://') &&
415
- target.type() === 'page');
416
- });
417
- for (const target of extensionTargets) {
418
- // Right now target.page() returns null for popup and side panel pages.
419
- let page = await target.page();
420
- if (!page) {
421
- // We need to cache pages instances for targets because target.asPage()
422
- // returns a new page instance every time.
423
- page = this.#extensionPages.get(target) ?? null;
424
- if (!page) {
425
- try {
426
- page = await target.asPage();
427
- this.#extensionPages.set(target, page);
428
- }
429
- catch (e) {
430
- this.logger('Failed to get page for extension target', e);
431
- }
432
- }
433
- }
434
- if (page && !allPages.includes(page)) {
435
- allPages.push(page);
436
- }
437
- }
348
+ #getBrowserContextToNameMap() {
438
349
  // Build a reverse lookup from BrowserContext instance → name.
439
350
  const contextToName = new Map();
440
351
  for (const [name, ctx] of this.#isolatedContexts) {
441
352
  contextToName.set(ctx, name);
442
353
  }
354
+ const defaultCtx = this.browser.defaultBrowserContext();
443
355
  // Auto-discover BrowserContexts not in our mapping (e.g., externally
444
356
  // created incognito contexts) and assign generated names.
445
357
  const knownContexts = new Set(this.#isolatedContexts.values());
@@ -450,40 +362,77 @@ export class McpContext {
450
362
  contextToName.set(ctx, name);
451
363
  }
452
364
  }
453
- // Map each page to its isolated context name (if any).
454
- const isolatedContextNames = new Map();
455
- for (const page of allPages) {
456
- const ctx = page.browserContext();
457
- const name = contextToName.get(ctx);
458
- if (name) {
459
- isolatedContextNames.set(page, name);
460
- }
365
+ return contextToName;
366
+ }
367
+ async #createMcpPage(page) {
368
+ let mcpPage = this.#mcpPages.get(page);
369
+ if (!mcpPage) {
370
+ mcpPage = new McpPage(page, nextPageId++, {
371
+ locatorClass: this.#locatorClass,
372
+ hasNetworkBlockOrAllowlist: this.#hasNetworkBlockOrAllowlist,
373
+ isolatedContextName: this.#getBrowserContextToNameMap().get(page.browserContext()),
374
+ navigationTimeout: this.#options.navigationTimeout,
375
+ });
376
+ this.#mcpPages.set(page, mcpPage);
377
+ await mcpPage.init();
461
378
  }
462
- return { pages: allPages, isolatedContextNames };
379
+ return mcpPage;
463
380
  }
464
- async detectOpenDevToolsWindows() {
465
- this.logger('Detecting open DevTools windows');
466
- const { pages } = await this.#getAllPages();
467
- await Promise.all(pages.map(async (page) => {
468
- const mcpPage = this.#mcpPages.get(page);
469
- if (!mcpPage) {
470
- return;
381
+ async createPagesSnapshot() {
382
+ const allPages = await this.#fetchBrowserPages();
383
+ await Promise.allSettled(allPages.map(page => this.#createMcpPage(page)));
384
+ // Prune orphaned #mcpPages entries (pages that no longer exist).
385
+ const currentPages = new Set(allPages);
386
+ for (const [page, mcpPage] of this.#mcpPages) {
387
+ if (!currentPages.has(page)) {
388
+ mcpPage.dispose();
389
+ this.#mcpPages.delete(page);
471
390
  }
472
- // Prior to Chrome 144.0.7559.59, the command fails,
473
- // Some Electron apps still use older version
474
- // Fall back to not exposing DevTools at all.
391
+ }
392
+ const pages = Array.from(this.#mcpPages.values());
393
+ // Only fall back when the selected page is actually gone. Gating on
394
+ // `isClosed()` instead of `pages` membership avoids silently swapping a
395
+ // live page that is momentarily missing from the snapshot.
396
+ this.#selectedPageFallback = undefined;
397
+ if ((!this.#selectedPage || this.#selectedPage.pptrPage.isClosed()) &&
398
+ pages[0]) {
399
+ // Record the automatic change so the response can surface it. Skipped on
400
+ // first connect, when there was no prior selection to replace.
401
+ if (this.#selectedPage) {
402
+ this.#selectedPageFallback = {
403
+ wasClosed: this.#selectedPage.pptrPage.isClosed(),
404
+ };
405
+ }
406
+ this.selectPage(pages[0]);
407
+ }
408
+ return pages.map(p => p.pptrPage);
409
+ }
410
+ async #fetchBrowserPages() {
411
+ const allPages = (await this.browser.pages(this.#options.experimentalIncludeAllPages)).filter(page => {
412
+ return (this.#options.experimentalDevToolsDebugging ||
413
+ !page.url().startsWith('devtools://'));
414
+ });
415
+ const allTargets = this.browser.targets();
416
+ const extensionTargets = allTargets.filter(target => {
417
+ return (target.url().startsWith('chrome-extension://') &&
418
+ target.type() === 'page');
419
+ });
420
+ await Promise.allSettled(extensionTargets.map(async (target) => {
475
421
  try {
476
- if (await page.hasDevTools()) {
477
- mcpPage.devToolsPage = await page.openDevTools();
422
+ let page = await target.page();
423
+ if (!page) {
424
+ page = await target.asPage();
478
425
  }
479
- else {
480
- mcpPage.devToolsPage = undefined;
426
+ this.#extensionPages.set(target, page);
427
+ if (page && !allPages.includes(page)) {
428
+ allPages.push(page);
481
429
  }
482
430
  }
483
- catch {
484
- mcpPage.devToolsPage = undefined;
431
+ catch (e) {
432
+ this.logger?.('Failed to get page for extension target', e);
485
433
  }
486
434
  }));
435
+ return allPages;
487
436
  }
488
437
  getExtensionServiceWorkers() {
489
438
  return this.#extensionServiceWorkers;
@@ -491,35 +440,37 @@ export class McpContext {
491
440
  getExtensionServiceWorkerId(extensionServiceWorker) {
492
441
  return this.#extensionServiceWorkerMap.get(extensionServiceWorker.target);
493
442
  }
494
- getPages() {
495
- return this.#pages;
496
- }
497
- getIsolatedContextName(page) {
498
- return this.#mcpPages.get(page)?.isolatedContextName;
499
- }
500
- async saveTemporaryFile(data, filename) {
501
- const filepath = await getTempFilePath(filename);
502
- this.validatePath(filepath);
443
+ async #writeFile(filepath, data) {
444
+ await this.validatePath(filepath);
503
445
  try {
504
- await fs.writeFile(filepath, data);
446
+ await fs.mkdir(path.dirname(filepath), { recursive: true });
447
+ // Open the file with flags to:
448
+ // - O_WRONLY: Write-only
449
+ // - O_CREAT: Create if it doesn't exist
450
+ // - O_TRUNC: Truncate to zero length if it exists
451
+ // - O_NOFOLLOW: DO NOT follow symlinks.
452
+ // - 0o600: Permissions: read/write for owner, no permissions for others.
453
+ await fs.writeFile(filepath, data, {
454
+ flag: fs.constants.O_WRONLY |
455
+ fs.constants.O_CREAT |
456
+ fs.constants.O_TRUNC |
457
+ fs.constants.O_NOFOLLOW,
458
+ mode: 0o600,
459
+ });
505
460
  }
506
461
  catch (err) {
507
- throw new Error('Could not save a file', { cause: err });
462
+ throw new Error(`Could not write ${filepath}`, { cause: err });
508
463
  }
464
+ }
465
+ async saveTemporaryFile(data, filename) {
466
+ const filepath = await getTempFilePath(filename);
467
+ await this.#writeFile(filepath, data);
509
468
  return { filepath };
510
469
  }
511
470
  async saveFile(data, clientProvidedFilePath, extension) {
512
- this.validatePath(clientProvidedFilePath);
513
- try {
514
- const filePath = ensureExtension(path.resolve(clientProvidedFilePath), extension);
515
- await fs.mkdir(path.dirname(filePath), { recursive: true });
516
- await fs.writeFile(filePath, data);
517
- return { filename: filePath };
518
- }
519
- catch (err) {
520
- this.logger(err);
521
- throw new Error('Could not save a file', { cause: err });
522
- }
471
+ const filePath = await this.ensureExtension(clientProvidedFilePath, extension);
472
+ await this.#writeFile(filePath, data);
473
+ return { filename: filePath };
523
474
  }
524
475
  storeTraceRecording(result) {
525
476
  // Clear the trace results because we only consume the latest trace currently.
@@ -529,45 +480,18 @@ export class McpContext {
529
480
  recordedTraces() {
530
481
  return this.#traceResults;
531
482
  }
532
- getNetworkRequestStableId(request) {
533
- return this.#networkCollector.getIdForResource(request);
534
- }
535
- waitForTextOnPage(text, timeout, targetPage) {
536
- const page = targetPage ?? this.getSelectedPptrPage();
537
- const frames = page.frames();
538
- let locator = this.#locatorClass.race(frames.flatMap(frame => text.flatMap(value => [
539
- frame.locator(`aria/${value}`),
540
- frame.locator(`text/${value}`),
541
- ])));
542
- if (timeout) {
543
- locator = locator.setTimeout(timeout);
544
- }
545
- return locator.wait();
546
- }
547
- /**
548
- * We need to ignore favicon request as they make our test flaky
549
- */
550
- async setUpNetworkCollectorForTesting() {
551
- this.#networkCollector = new NetworkCollector(this.browser, collect => {
552
- return {
553
- request: req => {
554
- if (req.url().includes('favicon.ico')) {
555
- return;
556
- }
557
- collect(req);
558
- },
559
- };
560
- });
561
- const { pages } = await this.#getAllPages();
562
- await this.#networkCollector.init(pages);
563
- }
564
483
  async installExtension(extensionPath) {
565
- this.validatePath(extensionPath);
566
- const id = await this.browser.installExtension(extensionPath);
484
+ const id = await Promise.race([
485
+ this.browser.installExtension(extensionPath),
486
+ new Promise((_, rej) => setTimeout(() => rej(new Error('Timeout installing extension')), 30000)),
487
+ ]);
567
488
  return id;
568
489
  }
569
490
  async uninstallExtension(id) {
570
- await this.browser.uninstallExtension(id);
491
+ await Promise.race([
492
+ this.browser.uninstallExtension(id),
493
+ new Promise((_, rej) => setTimeout(() => rej(new Error('Timeout uninstalling extension')), 30000)),
494
+ ]);
571
495
  }
572
496
  async triggerExtensionAction(id) {
573
497
  const extensions = await this.browser.extensions();
@@ -575,7 +499,7 @@ export class McpContext {
575
499
  if (!extension) {
576
500
  throw new Error(`Extension with ID ${id} not found.`);
577
501
  }
578
- const page = this.getSelectedPptrPage();
502
+ const page = this.getSelectedMcpPage().pptrPage;
579
503
  await extension.triggerAction(page);
580
504
  }
581
505
  listExtensions() {
@@ -585,21 +509,97 @@ export class McpContext {
585
509
  const pptrExtensions = await this.browser.extensions();
586
510
  return pptrExtensions.get(id);
587
511
  }
588
- async getHeapSnapshotAggregates(filePath) {
589
- this.validatePath(filePath);
590
- return await this.#heapSnapshotManager.getAggregates(filePath);
512
+ async getHeapSnapshotAggregates(filePath, filterName, objectId) {
513
+ return await this.#heapSnapshotManager.getAggregates(filePath, filterName, objectId);
514
+ }
515
+ async getHeapSnapshotDuplicateStrings(filePath) {
516
+ return await this.#heapSnapshotManager.getDuplicateStrings(filePath);
591
517
  }
592
518
  async getHeapSnapshotStats(filePath) {
593
- this.validatePath(filePath);
594
519
  return await this.#heapSnapshotManager.getStats(filePath);
595
520
  }
596
521
  async getHeapSnapshotStaticData(filePath) {
597
- this.validatePath(filePath);
598
522
  return await this.#heapSnapshotManager.getStaticData(filePath);
599
523
  }
600
- async getHeapSnapshotNodesByUid(filePath, uid) {
601
- this.validatePath(filePath);
602
- return await this.#heapSnapshotManager.getNodesByUid(filePath, uid);
524
+ async getHeapSnapshotNativeContextSizes(filePath) {
525
+ return await this.#heapSnapshotManager.getNativeContextSizes(filePath);
526
+ }
527
+ async getHeapSnapshotRetainedByContextSummary(filePath) {
528
+ return await this.#heapSnapshotManager.getRetainedByContextSummary(filePath);
529
+ }
530
+ async getHeapSnapshotNodesById(filePath, id, filterName, objectId) {
531
+ return await this.#heapSnapshotManager.getNodesById(filePath, id, filterName, objectId);
532
+ }
533
+ async getHeapSnapshotRetainers(filePath, nodeId) {
534
+ return await this.#heapSnapshotManager.getRetainers(filePath, nodeId);
535
+ }
536
+ async getHeapSnapshotObjectDetails(filePath, nodeId) {
537
+ return await this.#heapSnapshotManager.getObjectInfo(filePath, nodeId);
538
+ }
539
+ async closeHeapSnapshot(filePath) {
540
+ return this.#heapSnapshotManager.disposeSnapshot(filePath);
541
+ }
542
+ hasHeapSnapshots() {
543
+ return this.#heapSnapshotManager.hasSnapshots();
544
+ }
545
+ async getHeapSnapshotRetainingPaths(filePath, nodeId, maxDepth, maxNodes, maxSiblings) {
546
+ return await this.#heapSnapshotManager.getRetainingPaths(filePath, nodeId, maxDepth, maxNodes, maxSiblings);
547
+ }
548
+ async getHeapSnapshotDominators(filePath, nodeId) {
549
+ return await this.#heapSnapshotManager.getDominatorsOf(filePath, nodeId);
550
+ }
551
+ #validateUrlNotBlocked(url) {
552
+ if (!this.#options.blocklist) {
553
+ return;
554
+ }
555
+ for (const block of this.#options.blocklist) {
556
+ const pattern = new URLPattern(block);
557
+ if (pattern.test(url)) {
558
+ throw new Error(`Blocked by blocklist: ${url}`);
559
+ }
560
+ }
561
+ }
562
+ #validateUrlAllowed(url) {
563
+ if (!this.#options.allowList) {
564
+ return;
565
+ }
566
+ for (const allow of this.#options.allowList) {
567
+ const pattern = new URLPattern(allow);
568
+ if (pattern.test(url)) {
569
+ return;
570
+ }
571
+ }
572
+ throw new Error(`Not allowed by allowlist: ${url}`);
573
+ }
574
+ async loadResource(path) {
575
+ const url = new URL(path);
576
+ this.#validateUrlNotBlocked(url);
577
+ switch (url.protocol) {
578
+ case 'https:':
579
+ case 'http:': {
580
+ this.#validateUrlAllowed(url);
581
+ const response = await fetch(url);
582
+ if (!response.ok) {
583
+ throw new Error(`Failed to load resource: ${url}`);
584
+ }
585
+ return response.text();
586
+ }
587
+ case 'file:': {
588
+ await this.validatePath(fileURLToPath(url));
589
+ return await fs.readFile(url, 'utf-8');
590
+ }
591
+ default:
592
+ throw new Error(`Unsupported protocol for: ${url}`);
593
+ }
594
+ }
595
+ async getHeapSnapshotEdges(filePath, nodeId, options) {
596
+ return await this.#heapSnapshotManager.getEdges(filePath, nodeId, options);
597
+ }
598
+ async getHeapSnapshotClassDiffs(baseFilePath, currentFilePath) {
599
+ return await this.#heapSnapshotManager.getClassDiffs(baseFilePath, currentFilePath);
600
+ }
601
+ async getHeapSnapshotDetailedClassDiff(baseFilePath, currentFilePath, classIndex) {
602
+ return await this.#heapSnapshotManager.getDetailedClassDiff(baseFilePath, currentFilePath, classIndex);
603
603
  }
604
604
  }
605
605
  //# sourceMappingURL=McpContext.js.map