@mozilla/firefox-devtools-mcp-moz 0.9.15 → 0.10.1

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.
@@ -1 +1 @@
1
- "use strict";var __SnapshotInjected=(()=>{var N=Object.defineProperty;var B=Object.getOwnPropertyDescriptor;var J=Object.getOwnPropertyNames;var K=Object.prototype.hasOwnProperty;var Q=(e,n)=>{for(var t in n)N(e,t,{get:n[t],enumerable:!0})},Y=(e,n,t,r)=>{if(n&&typeof n=="object"||typeof n=="function")for(let o of J(n))!K.call(e,o)&&o!==t&&N(e,o,{get:()=>n[o],enumerable:!(r=B(n,o))||r.enumerable});return e};var Z=e=>Y(N({},"__esModule",{value:!0}),e);var ue={};Q(ue,{createSnapshot:()=>U});var T=["a","button","input","select","textarea","img","video","audio","iframe"],ee=["nav","main","section","article","header","footer","form"],te=["div","span","p","li","ul","ol"];function y(e){if(e?.nodeType!==Node.ELEMENT_NODE)return!1;let n=e;for(;n&&n!==document.documentElement;){try{let t=window.getComputedStyle(n),r=parseFloat(t.opacity);if(t.display==="none"||t.visibility==="hidden"||r===0||isNaN(r))return!1}catch{return!1}n=n.parentElement}return!0}function ne(e){let n="";for(let t=0;t<e.childNodes.length;t++){let r=e.childNodes[t];r?.nodeType===Node.TEXT_NODE&&(n+=r.textContent||"")}return n.trim()}function re(e){for(let n=0;n<e.children.length;n++){let t=e.children[n];if(t){let r=t.tagName.toLowerCase();if(T.indexOf(r)!==-1||t.hasAttribute("role"))return!0}}return!1}function R(e){if(e?.nodeType!==Node.ELEMENT_NODE||!y(e))return!1;let n=e.tagName.toLowerCase();if(T.indexOf(n)!==-1||e.hasAttribute("role")||e.hasAttribute("aria-label")||/^h[1-6]$/.test(n)||ee.indexOf(n)!==-1)return!0;if(te.indexOf(n)!==-1){let t=ne(e);if(t.length>0&&t.length<500||e.id||e.className||re(e))return!0}return!1}function $(e){if(e.tabIndex>=0)return!0;let t=e.tagName.toLowerCase();return["a","button","input","select","textarea"].indexOf(t)!==-1}function D(e){let n=e.tagName.toLowerCase();if(T.indexOf(n)!==-1)return!0;let t=e.getAttribute("role");return!!(t&&["button","link","menuitem","tab"].indexOf(t)!==-1||e.hasAttribute("onclick"))}var ie=100;function X(e){if(e.hasAttribute("aria-label"))return e.getAttribute("aria-label")||void 0;let t=e.id;if(t){let o=document.querySelector(`label[for="${t}"]`);if(o?.textContent)return o.textContent.trim()}if(e.hasAttribute("placeholder"))return e.getAttribute("placeholder")||void 0;if(e.hasAttribute("title"))return e.getAttribute("title")||void 0;if(e.hasAttribute("alt"))return e.getAttribute("alt")||void 0;let r=e.tagName.toLowerCase();if(["button","a","h1","h2","h3","h4","h5","h6"].indexOf(r)!==-1)return x(e)}function x(e){let n="";for(let r=0;r<e.childNodes.length;r++){let o=e.childNodes[r];o?.nodeType===Node.TEXT_NODE&&(n+=o.textContent||"")}let t=n.trim();if(t)return t.substring(0,ie)}function W(e){let n={},t=!1,r=["disabled","hidden","selected","expanded"];for(let i of r){let a=e.getAttribute(`aria-${i}`);a!==null&&(n[i]=a==="true",t=!0)}let o=["checked","pressed"];for(let i of o){let a=e.getAttribute(`aria-${i}`);a!==null&&(a==="mixed"?n[i]="mixed":n[i]=a==="true",t=!0)}let l=["autocomplete","haspopup","invalid","label","labelledby","describedby","controls"];for(let i of l){let a=e.getAttribute(`aria-${i}`);a&&(n[i]=a,t=!0)}let u=e.getAttribute("aria-level");if(u){let i=parseInt(u,10);isNaN(i)||(n.level=i,t=!0)}return t?n:void 0}function G(e){let n={};try{let t=window.getComputedStyle(e),r=parseFloat(t.opacity);n.visible=t.display!=="none"&&t.visibility!=="hidden"&&r!==0&&!isNaN(r)}catch{n.visible=!1}return n.accessible=n.visible&&!e.getAttribute("aria-hidden"),n.focusable=$(e),n.interactive=D(e),n}var oe=["id","data-testid","data-test-id"];function P(e){let n=[],t=e;for(;t?.nodeType===Node.ELEMENT_NODE;){let r=t.nodeName.toLowerCase(),o=!1;for(let a of oe){let d=t.getAttribute(a);if(d){a==="id"?r+="#"+CSS.escape(d):r+=`[${a}="${H(d)}"]`,n.unshift(r),o=!0;break}}if(o)break;let l=t.getAttribute("aria-label"),u=t.getAttribute("role");if(l&&u){r+=`[role="${u}"][aria-label="${H(l)}"]`,n.unshift(r),t=t.parentElement;continue}let i=t.parentElement?.children;if(i&&i.length>1){let a=1;for(let d=0;d<i.length;d++){let s=i[d];if(s){if(s===t)break;s.nodeName===t.nodeName&&a++}}(a>1||i.length>1&&i[0]!==t)&&(r+=`:nth-of-type(${a})`)}if(n.unshift(se(r)),t=t.parentElement,t?.nodeName.toLowerCase()==="body"){n.unshift("body");break}}return n.join(" > ")}function F(e){let n=e.id;if(n)return`//*[@id="${ae(n)}"]`;let t=[],r=e;for(;r?.nodeType===Node.ELEMENT_NODE;){let o=r.nodeName.toLowerCase(),l=1,u=r.previousElementSibling;for(;u;)u.nodeName.toLowerCase()===o&&l++,u=u.previousElementSibling;let i=r.parentElement,a=!1;i&&(a=Array.from(i.children).filter(h=>h.nodeName.toLowerCase()===o).length>1);let d=a?`${o}[${l}]`:o;if(t.unshift(d),r=r.parentElement,r?.nodeName.toLowerCase()==="html"){t.unshift("html");break}}return"/"+t.join("/")}function H(e){return e.replace(/"/g,'\\"').substring(0,64)}function ae(e){return e.indexOf('"')===-1||e.indexOf("'")===-1?e:`concat(${e.split('"').map((t,r,o)=>r===o.length-1?t?`"${t}"`:"":t?`"${t}",'"'`:`"'"`).filter(t=>t).join(",")})`}function se(e){return e.length<=64?e:e.substring(0,64)}var le=10,j=1e3;function V(e,n,t={}){let{includeAll:r=!1,includeIframes:o=!0}=t,l=0,u=[],i=!1;function a(s,h){if(h>le)return i=!0,{node:null,relevantChildren:[]};if(l>=j)return i=!0,{node:null,relevantChildren:[]};let b=s.tagName.toLowerCase(),C=b==="body"||b==="html",g;r?g=C||y(s):g=C||R(s);let E=[];if(b==="iframe"&&o&&g)try{let c=s,p=c.contentDocument||c.contentWindow?.document;if(p?.body){let f=a(p.body,h+1);f.node&&(f.node.isIframe=!0,f.node.frameSrc=c.src,E.push(f.node))}}catch{}else for(let c=0;c<s.children.length;c++){if(l>=j){i=!0;break}let p=s.children[c];if(!p)continue;let f=a(p,h+1);f.node?E.push(f.node):f.relevantChildren.length>0&&E.push(...f.relevantChildren)}if(!g)return{node:null,relevantChildren:E};let S=`${n}_${l++}`,q=P(s),z=F(s);u.push({uid:S,css:q,xpath:z});let A=s,v=s.getAttribute("role"),O=X(s),_=x(s),w=A.value,M=A.href,L=A.src,I=W(s),k=G(s),m={uid:S,tag:b,...v&&{role:v},...O&&{name:O},...w&&{value:w},...M&&{href:M},...L&&{src:L},..._&&{text:_},...I&&{aria:I},...k&&{computed:k},children:E};if(b==="iframe"&&o)try{let c=s;(c.contentDocument||c.contentWindow?.document)?.body||(m.isIframe=!0,m.frameSrc=c.src,m.crossOrigin=!0)}catch{m.isIframe=!0,m.frameSrc=s.src,m.crossOrigin=!0}return{node:m,relevantChildren:[]}}return{tree:a(e,0).node,uidMap:u,truncated:i}}function U(e,n){try{let t=document.body;if(n?.selector)try{let l=document.querySelector(n.selector);if(!l)return{tree:null,uidMap:[],truncated:!1,selectorError:`Selector "${n.selector}" not found`};t=l}catch{return{tree:null,uidMap:[],truncated:!1,selectorError:`Invalid selector syntax: "${n.selector}"`}}let r={includeIframes:n?.includeIframes??!0};n?.includeAll!==void 0&&(r.includeAll=n.includeAll);let o=V(t,e,r);if(!o.tree)throw new Error("Failed to generate tree");return o}catch{return{tree:null,uidMap:[],truncated:!1}}}typeof window<"u"&&(window.__createSnapshot=U);return Z(ue);})();
1
+ "use strict";var __SnapshotInjected=(()=>{var x=Object.defineProperty;var ne=Object.getOwnPropertyDescriptor;var re=Object.getOwnPropertyNames;var ie=Object.prototype.hasOwnProperty;var oe=(e,t)=>{for(var n in t)x(e,n,{get:t[n],enumerable:!0})},se=(e,t,n,r)=>{if(t&&typeof t=="object"||typeof t=="function")for(let o of re(t))!ie.call(e,o)&&o!==n&&x(e,o,{get:()=>t[o],enumerable:!(r=ne(t,o))||r.enumerable});return e};var ae=e=>se(x({},"__esModule",{value:!0}),e);var Ee={};oe(Ee,{clearUidRegistry:()=>te,createSnapshot:()=>Q,resolveUid:()=>Z,uidToSelector:()=>ee});var C=["a","button","input","select","textarea","img","video","audio","iframe"],le=["nav","main","section","article","header","footer","form"],ue=["div","span","p","li","ul","ol"];function v(e){if(e?.nodeType!==Node.ELEMENT_NODE)return!1;let t=e;for(;t&&t!==document.documentElement;){try{let n=window.getComputedStyle(t),r=parseFloat(n.opacity);if(n.display==="none"||n.visibility==="hidden"||r===0||isNaN(r))return!1}catch{return!1}t=t.parentElement}return!0}function ce(e){let t="";for(let n=0;n<e.childNodes.length;n++){let r=e.childNodes[n];r?.nodeType===Node.TEXT_NODE&&(t+=r.textContent||"")}return t.trim()}function de(e){for(let t=0;t<e.children.length;t++){let n=e.children[t];if(n){let r=n.tagName.toLowerCase();if(C.indexOf(r)!==-1||n.hasAttribute("role"))return!0}}return!1}function X(e){if(e?.nodeType!==Node.ELEMENT_NODE||!v(e))return!1;let t=e.tagName.toLowerCase();if(C.indexOf(t)!==-1||e.hasAttribute("role")||e.hasAttribute("aria-label")||/^h[1-6]$/.test(t)||le.indexOf(t)!==-1)return!0;if(ue.indexOf(t)!==-1){let n=ce(e);if(n.length>0&&n.length<500||e.id||e.className||de(e))return!0}return!1}function G(e){if(e.tabIndex>=0)return!0;let n=e.tagName.toLowerCase();return["a","button","input","select","textarea"].indexOf(n)!==-1}function $(e){let t=e.tagName.toLowerCase();if(C.indexOf(t)!==-1)return!0;let n=e.getAttribute("role");return!!(n&&["button","link","menuitem","tab"].indexOf(n)!==-1||e.hasAttribute("onclick"))}var fe=100;function H(e){if(e.hasAttribute("aria-label"))return e.getAttribute("aria-label")||void 0;let n=e.id;if(n){let o=document.querySelector(`label[for="${n}"]`);if(o?.textContent)return o.textContent.trim()}if(e.hasAttribute("placeholder"))return e.getAttribute("placeholder")||void 0;if(e.hasAttribute("title"))return e.getAttribute("title")||void 0;if(e.hasAttribute("alt"))return e.getAttribute("alt")||void 0;let r=e.tagName.toLowerCase();if(["button","a","h1","h2","h3","h4","h5","h6"].indexOf(r)!==-1)return R(e)}function R(e){let t="";for(let r=0;r<e.childNodes.length;r++){let o=e.childNodes[r];o?.nodeType===Node.TEXT_NODE&&(t+=o.textContent||"")}let n=t.trim();if(n)return n.substring(0,fe)}function F(e){let t={},n=!1,r=["disabled","hidden","selected","expanded"];for(let i of r){let s=e.getAttribute(`aria-${i}`);s!==null&&(t[i]=s==="true",n=!0)}let o=["checked","pressed"];for(let i of o){let s=e.getAttribute(`aria-${i}`);s!==null&&(s==="mixed"?t[i]="mixed":t[i]=s==="true",n=!0)}let l=["autocomplete","haspopup","invalid","label","labelledby","describedby","controls"];for(let i of l){let s=e.getAttribute(`aria-${i}`);s&&(t[i]=s,n=!0)}let u=e.getAttribute("aria-level");if(u){let i=parseInt(u,10);isNaN(i)||(t.level=i,n=!0)}return n?t:void 0}function P(e){let t={};try{let n=window.getComputedStyle(e),r=parseFloat(n.opacity);t.visible=n.display!=="none"&&n.visibility!=="hidden"&&r!==0&&!isNaN(r)}catch{t.visible=!1}return t.accessible=t.visible&&!e.getAttribute("aria-hidden"),t.focusable=G(e),t.interactive=$(e),t}var V="__uidRegistry";function A(){let e=window,t=e[V];return t||(t={uidToElement:new Map,elementToUid:new WeakMap},e[V]=t),t}function j(e){return A().elementToUid.get(e)}function q(e){let t=A();for(let[n,r]of e)t.uidToElement.set(n,new WeakRef(r)),t.elementToUid.set(r,n)}function _(e){let t=A(),n=t.uidToElement.get(e)?.deref();return n?.isConnected?n:(t.uidToElement.delete(e),null)}function Y(){let e=A();e.uidToElement.clear(),e.elementToUid=new WeakMap}var me=10,z=1e3;function B(e,t,n={}){let{includeAll:r=!1,includeIframes:o=!0}=n,l=0,u=t,i=!1,s=new Map;function c(a,h){if(h>me)return i=!0,{node:null,relevantChildren:[]};if(l>=z)return i=!0,{node:null,relevantChildren:[]};let S=h===0,b;r?b=S||v(a):b=S||X(a);let g=[],y=a.tagName.toLowerCase();if(y==="iframe"&&o&&b)try{let d=a,p=d.contentDocument||d.contentWindow?.document;if(p?.body){let f=c(p.body,h+1);f.node&&(f.node.isIframe=!0,f.node.frameSrc=d.src,g.push(f.node))}}catch{}else for(let d=0;d<a.children.length;d++){if(l>=z){i=!0;break}let p=a.children[d];if(!p)continue;let f=c(p,h+1);f.node?g.push(f.node):f.relevantChildren.length>0&&g.push(...f.relevantChildren)}if(!b)return{node:null,relevantChildren:g};let T=j(a);T===void 0&&(T=`e${u++}`,s.set(T,a)),l++;let N=a,w=a.id,k=a.getAttribute("role"),M=H(a),O=R(a),I=N.value,L=N.href,U=N.src,W=F(a),D=P(a),m={uid:T,tag:y,...w&&{id:w},...k&&{role:k},...M&&{name:M},...I&&{value:I},...L&&{href:L},...U&&{src:U},...O&&{text:O},...W&&{aria:W},...D&&{computed:D},children:g};if(y==="iframe"&&o)try{let d=a;(d.contentDocument||d.contentWindow?.document)?.body||(m.isIframe=!0,m.frameSrc=d.src,m.crossOrigin=!0)}catch{m.isIframe=!0,m.frameSrc=a.src,m.crossOrigin=!0}return{node:m,relevantChildren:[]}}return{tree:c(e,0).node,nodeCount:l,truncated:i,nextElementId:u,newUids:s}}var pe=["id","data-testid","data-test-id"];function K(e){let t=[],n=e;for(;n?.nodeType===Node.ELEMENT_NODE;){let r=n.nodeName.toLowerCase(),o=!1;for(let s of pe){let c=n.getAttribute(s);if(c){s==="id"?r+="#"+CSS.escape(c):r+=`[${s}="${J(c)}"]`,t.unshift(r),o=!0;break}}if(o)break;let l=n.getAttribute("aria-label"),u=n.getAttribute("role");if(l&&u){r+=`[role="${u}"][aria-label="${J(l)}"]`,t.unshift(r),n=n.parentElement;continue}let i=n.parentElement?.children;if(i&&i.length>1){let s=1;for(let c=0;c<i.length;c++){let E=i[c];if(E){if(E===n)break;E.nodeName===n.nodeName&&s++}}(s>1||i.length>1&&i[0]!==n)&&(r+=`:nth-of-type(${s})`)}if(t.unshift(ge(r)),n=n.parentElement,n?.nodeName.toLowerCase()==="body"){t.unshift("body");break}}return t.join(" > ")}function J(e){return e.replace(/"/g,'\\"').substring(0,64)}function ge(e){return e.length<=64?e:e.substring(0,64)}function Q(e,t){try{let n=document.body;if(t?.selector)try{let u=document.querySelector(t.selector);if(!u)return{tree:null,nodeCount:0,truncated:!1,nextElementId:e,selectorError:`Selector "${t.selector}" not found`};n=u}catch{return{tree:null,nodeCount:0,truncated:!1,nextElementId:e,selectorError:`Invalid selector syntax: "${t.selector}"`}}let r={includeIframes:t?.includeIframes??!0};t?.includeAll!==void 0&&(r.includeAll=t.includeAll);let{newUids:o,...l}=B(n,e,r);if(!l.tree)throw new Error("Failed to generate tree");return q(o),l}catch{return{tree:null,nodeCount:0,truncated:!1,nextElementId:e}}}function Z(e){return _(e)}function ee(e){let t=_(e);return t?K(t):null}function te(){Y()}if(typeof window<"u"){let e=window;e.__createSnapshot=Q,e.__resolveUid=Z,e.__uidToSelector=ee,e.__clearUidRegistry=te}return ae(Ee);})();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mozilla/firefox-devtools-mcp-moz",
3
- "version": "0.9.15",
3
+ "version": "0.10.1",
4
4
  "description": "Model Context Protocol (MCP) server for Firefox DevTools automation (moz build with privileged context support)",
5
5
  "author": "Mozilla",
6
6
  "license": "MIT OR Apache-2.0",
@@ -1,6 +1,8 @@
1
1
  ---
2
+ name: debug
2
3
  description: Show console errors and failed network requests
3
4
  argument-hint: [console|network|all]
5
+ disable-model-invocation: true
4
6
  ---
5
7
 
6
8
  # /firefox-devtools-mcp:debug
@@ -2,6 +2,7 @@
2
2
  name: navigate
3
3
  description: Navigate Firefox to a URL and take a DOM snapshot for interaction
4
4
  argument-hint: <url>
5
+ disable-model-invocation: true
5
6
  ---
6
7
 
7
8
  # /firefox-devtools-mcp:navigate
@@ -1,7 +1,8 @@
1
1
  ---
2
2
  name: screenshot
3
- description: Take a screenshot of a URL or the current page. Use when the user asks to capture, screenshot, or photograph a web page or URL.
3
+ description: Take a screenshot of a URL or the current page
4
4
  argument-hint: [url or uid]
5
+ disable-model-invocation: true
5
6
  ---
6
7
 
7
8
  # /firefox-devtools-mcp:screenshot
@@ -0,0 +1,61 @@
1
+ ---
2
+ name: web-performance
3
+ description: Find and fix why a website is slow by capturing and analyzing a Firefox performance profile, or by analyzing a profile the user already has (a saved file or a profiler.firefox.com share link). Use for slow page loads, janky scrolling or animation, slow interactions or a slow STR, long tasks, layout thrashing, or heavy JavaScript.
4
+ ---
5
+
6
+ Work from a real profile, find the cause in the user's code, report it honestly, and if asked, fix it and prove the fix worked. The firefox-devtools MCP drives Firefox and records a profile when there is none yet; `profiler-cli` queries the profile, whether you captured it or the user brought it. Report in web-platform terms (LCP, main-thread blocking, reflow, render-blocking resources), not Gecko internals - unless the target is Firefox itself rather than a page, in which case platform frames are the subject and the fix lands in mozilla-central.
7
+
8
+ ## Step 0: Prerequisites
9
+
10
+ profiler-cli needs Node.js >= 24:
11
+ ```bash
12
+ command -v profiler-cli >/dev/null 2>&1 || npm install -g @firefox-devtools/profiler-cli@latest
13
+ ```
14
+ `npx @firefox-devtools/profiler-cli@latest` also works but re-resolves on every call.
15
+
16
+ The rest of this step is only for capturing. Skip it when the user already has a profile.
17
+
18
+ The profiler tools need **Firefox 154+**. Check with `get_firefox_info`, which also launches it. If Firefox is missing or older, stop and ask the user to install a current release or point the server at one with `--firefox-path`. The profiler tools also need the `developer` tool preset: if `profiler_start` is absent, ask the user to restart the server with `--tool-preset developer`, since the default `basic` preset has no profiler.
19
+
20
+ ## Step 1: Pick the scenario
21
+
22
+ **If the user already has a profile** - a saved `.json.gz`, or a `share.firefox.dev` / profiler.firefox.com link - there is nothing to capture: skip to Step 3 and `load` it directly. Ask what they were doing while it recorded, since the capture type decides the entry point in the playbook. Verifying a fix (Step 6) still needs a capture, so if you cannot reach their setup, hand them the recipe and ask for an after profile.
23
+
24
+ Otherwise ask if unclear: **page load**, **interaction / STR** (one slow action), or **ongoing jank** (scroll stutter, dropped frames, CPU pinning). Each has a recipe in `references/capture-recipes.md`.
25
+
26
+ ## Step 2: Capture
27
+
28
+ Follow that recipe. Keep the window tight - start late, stop early - and keep the profile path `profiler_stop` returns.
29
+
30
+ ## Step 3: Analyze
31
+
32
+ 1. Run `profiler-cli guide` and read the **entire** output; it is the command reference for everything below. Do not skim, and make sure that Bash did not truncate it.
33
+ 2. `profiler-cli load <path>`, or `profiler-cli load <share-url>` for a profile the user shared.
34
+ 3. Work through `references/analysis-playbook.md`: thread selection, the entry point for each capture type, and a symptom-to-command map.
35
+
36
+ Run the commands and interpret the output yourself; do not print commands for the user to run.
37
+
38
+ ## Step 4: Find the cause in the source
39
+
40
+ `Grep`/`Glob`/`Read` the project for the code behind a hot function or request, and confirm the mechanism. If you cannot connect a finding to real source, say so instead of inventing a code path.
41
+
42
+ ## Step 5: Report with explicit confidence
43
+
44
+ Label every finding:
45
+ - **Confirmed** - in the profile and cross-checked (stack traced to source, a measured before/after, a `performance.measure` you captured). State the evidence.
46
+ - **Likely** - strong single-source evidence, not cross-checked. Say what would confirm it.
47
+ - **Hypothesis** - a plausible reading of ambiguous data. Say so, and how to validate it.
48
+
49
+ If sampling is sparse, the window was wrong, or the time is mostly idle, call it inconclusive and re-capture instead of guessing.
50
+
51
+ Per finding: what is slow -> evidence (function/marker/request and its cost) plus confidence -> why -> the fix in the developer's terms. Biggest confirmed wins first.
52
+
53
+ When a "likely" or "hypothesis" finding is worth acting on, get more evidence first. `references/validation.md` covers instrumenting the page with User Timing and reading navigation, resource, paint, LCP and Event Timing entries.
54
+
55
+ ## Step 6: Fix and verify
56
+
57
+ Implement only if asked, keeping the change minimal and tied to the confirmed finding. Then re-capture like-for-like (see the before/after section of the recipes) and compare the metric that was slow. Quantify the gain; if it did not improve, or something else regressed, say so and reconsider.
58
+
59
+ ## Step 7: Clean up
60
+
61
+ `profiler-cli stop` - the daemon holds a port and memory until stopped. Stop the Firefox profiler if `profiler_is_active`, and remove instrumentation you injected unless the user wants it kept.
@@ -0,0 +1,62 @@
1
+ # Analysis playbook
2
+
3
+ Turning a loaded profile into web-platform findings. Assumes `profiler-cli guide` has been read in full (it documents the flags used here) and the profile is loaded.
4
+
5
+ ## Select the right thread
6
+
7
+ Start on the content process main thread: the `GeckoMain` thread of the `Isolated Web Content [<url>]` process serving the page. Most of the script, layout and paint work a user feels is there. Cross-origin iframes get their own content process, so third-party work (ads, embeds) sits under a different one; worker cost sits on `DOM Worker` threads.
8
+
9
+ That is where to start, not where to stop. Depending on the problem:
10
+
11
+ - **Network.** The request is not content-process work at all. `GeckoMain` in the parent process drives navigation and channel setup, and `Socket Thread` does connection, TLS and transfer, with `DNS Resolver`, `TRR Background` and `Cache2 I/O` next to it - in the parent, or in a separate `Socket` process when the build runs one. A content thread sitting idle while requests are in flight means the answer is on those threads.
12
+ - **Scrolling, animation, dropped frames.** `Compositor`, `Renderer` and the GPU-process threads, alongside the content main thread.
13
+ - **Firefox itself as the target.** Any platform thread can be the subject, most often parent `GeckoMain`.
14
+
15
+ A thread is only in the profile if the capture recorded it: the `web-developer` preset leaves out the networking threads, so a network question usually needs a re-capture with `preset="networking"`.
16
+
17
+ ## Start here
18
+
19
+ **Page-load captures:** `thread page-load` gives navigation timing, FCP/LCP, resources, CPU and jank periods in one view, and its marker handles feed `zoom push` and `marker info`. "No page load markers found in this thread" means the capture has no navigation or the thread is wrong - it is not a verdict about the page.
20
+
21
+ **Interaction and jank captures** never navigate, so `page-load` returns nothing for them. Enter through markers:
22
+
23
+ ```
24
+ profiler-cli thread markers --min-duration 50 --list # long intervals, chronological
25
+ profiler-cli zoom push m-<handle> # onto the worst one
26
+ profiler-cli thread samples-top-down # what ran during it
27
+ ```
28
+
29
+ Blocking main-thread work appears as a long `Runnable`, or `Perform microtasks`. `DOMEvent` markers locate the interaction. Ignore long-lived span markers that are not blocking work: `Image Animation` (one animating GIF spans the whole recording), `IPC Accumulator`. `--has-stack` with `marker stack <handle>` gives the stack that belongs to a marker.
30
+
31
+ ## Minified bundles: apply the source map first
32
+
33
+ If JS frames show mangled names (`a`, `t.exports`), de-minify before reading stacks.
34
+
35
+ ```
36
+ profiler-cli sourcemap sources # bundles with a source map, as src-N, plus their sourceMapURL
37
+ profiler-cli sourcemap apply dist/bundle.js.map
38
+ ```
39
+
40
+ `apply` reads a local file, it does not fetch: take the `.map` from the user's build output, and if `sources` shows a remote `sourceMapURL`, download it first. It rewrites stacks in place, so apply before reading call trees. The guide's SOURCE MAPS section covers `--to src-N` and ambiguous matches. With no map, keep findings at function level - mangled names do not support line-level claims.
41
+
42
+ ## Symptom -> where to look
43
+
44
+ **Do not limit yourself to the categories below.** They are the common cases, not a list of the problems a profile can answer. The data decides what the problem is; if the symptom does not match any of them, or the profile points somewhere else, follow the profile. Forcing a finding into one of these buckets is how you end up reporting the wrong cause.
45
+
46
+ **Slow first paint / LCP / content appears late.** `thread page-load` for which phase dominates. Before first byte is server or network; between response and paint is client work. `thread markers` shows when paint, DOMContentLoaded and load actually fired.
47
+
48
+ **Long tasks / blocked main thread / unresponsive UI.** Take long tasks from the profile: `page-load` jank periods for a navigation capture, the marker route above otherwise. `samples-bottom-up` complements the top-down tree by showing hot leaf functions and their callers. If the hot frames are framework internals (render, reconcile, hydrate), look for too many or too expensive components rather than one slow function.
49
+
50
+ **Heavy JavaScript.** `thread functions` for a flat list by CPU percentage, `samples-top-down` for the tree, `function annotate <handle>` for per-line timing, `function expand <handle>` for a truncated name.
51
+
52
+ **Layout thrashing / forced reflow.** In `samples-top-down`, look for reflow/layout/style-recalc frames interleaved with script - that pattern is a script reading layout, writing, then reading again, forcing synchronous reflow. `thread markers` shows how often layout and reflow markers fire. Then find the read-write-read loop in the source.
53
+
54
+ **Slow or render-blocking network.** `thread network` gives per-request timing phases (DNS, connect, TLS, wait, download) for the selected thread. Look for render-blocking CSS/JS in the head, long TTFB, request chains where each waits on the previous, and duplicate downloads. When the cost is in the transfer itself rather than in how the page requested it, follow it onto the parent's `GeckoMain` and `Socket Thread` as described above. The profile has timing but not response headers, so for `content-encoding`, `cache-control` and `content-type` - what tells you whether an asset is actually compressed or cacheable - use the MCP's `list_network_requests` and `get_network_request`.
55
+
56
+ **User Timing.** `thread markers` shows the developer's `performance.mark`/`measure` alongside platform markers; `marker info` and `marker stack` give one marker's detail and its stack.
57
+
58
+ **Anything else.** Let the profile pick the direction: `profile info` for which process and thread actually burned CPU or stalled, then markers and samples on that thread.
59
+
60
+ ## Before recommending a fix
61
+
62
+ Correlate the hot stack with markers and network timing. A stack alone says what ran, rarely why it ran.
@@ -0,0 +1,53 @@
1
+ # Capture recipes
2
+
3
+ Record the right profile per scenario. If a capture misses the slow moment or is mostly idle, re-capture instead of salvaging it.
4
+
5
+ Applies to every recipe:
6
+ - Firefox launches lazily on the first MCP call (`list_pages`, `get_firefox_info`). Reuse the existing session and tab; only `new_page` / `navigate_page` for a different page or a clean state.
7
+ - `preset="web-developer"` is the usual choice for `profiler_start`, but it does not record the networking threads. A network-shaped problem (slow TTFB, connection or TLS setup, DNS, cache misses) might want `networking` instead. Use `firefox-platform` when the target is Firefox itself, another preset when the problem sits squarely in its domain, or explicit `entries`/`interval`/`features`/`threads` when none of them fit.
8
+ - `profiler_stop` saves to Firefox's downloads directory and returns the path. Keep it.
9
+ - If nothing records, check `profiler_is_active` and confirm Firefox is 154+.
10
+
11
+ ## Page load
12
+
13
+ The recording should span one navigation with nothing before it.
14
+
15
+ 1. `navigate_page url="about:blank"`, so the navigation is captured from the start.
16
+ 2. `profiler_start`.
17
+ 3. `navigate_page url="https://the-page"` - this navigation is the whole recording.
18
+ 4. Wait for the page to finish, then stop promptly without recording an idle tail. There is no wait tool, so poll: `evaluate_script function="() => [document.readyState, performance.getEntriesByType('navigation')[0]?.loadEventEnd]"`, or `screenshot_page`.
19
+ 5. `profiler_stop`.
20
+
21
+ Caching caveat: this is a clean navigation but not a cold load. The session shares Firefox's HTTP cache, and `about:blank` or a new tab does not clear it. Neither can you: there is no cache-clearing or private-window tool. So either:
22
+ - label the finding a warm-cache load, or
23
+ - for a real first visit, `restart_firefox` with `profilePath` set to a fresh empty directory - a new profile starts with an empty cache, but it closes every tab and drops cookies and logins, so it is no good for authenticated pages.
24
+
25
+ Confirm which one you got instead of assuming: in `thread network`, a cold load shows real DNS/connect/download phases for subresources, a warm one near-zero fetch time.
26
+
27
+ Warm variant (repeat visit): navigate to the URL and let it settle first, then start the profiler and navigate to it again. The second navigation is the measured one.
28
+
29
+ ## Interaction or STR
30
+
31
+ Record the action, not the setup.
32
+
33
+ 1. `navigate_page` to the state right before the slow action. Do NOT perform it yet.
34
+ 2. `take_snapshot` to resolve the UIDs you will need, so the recorded window is only the action.
35
+ 3. `profiler_start`.
36
+ 4. Perform exactly the steps the user reports as slow, in order, with the automation tools.
37
+ 5. `profiler_stop` as soon as the slow result is visible.
38
+
39
+ If the DOM changed and you need a fresh snapshot mid-recording, `take_snapshot` again; it does not meaningfully pollute the profile.
40
+
41
+ ## Ongoing jank (scroll, animation, CPU pinning)
42
+
43
+ 1. `navigate_page` to the janky page and state.
44
+ 2. `profiler_start`.
45
+ 3. Reproduce the jank for a few representative seconds. Drive the scrolling if you can, otherwise ask the user to scroll while recording.
46
+ 4. `profiler_stop`.
47
+
48
+ ## Re-capturing to verify a fix
49
+
50
+ The second capture must be like-for-like or the comparison is meaningless:
51
+ - Same recipe, URL, STR steps and window length.
52
+ - Same cache warmth and page state; a cold-vs-warm difference will swamp the fix.
53
+ - Ideally the same `performance.mark`/`measure` instrumentation (see `validation.md`), so you compare the same measured span rather than two eyeballed windows.
@@ -0,0 +1,35 @@
1
+ # Validation: turning guesses into measurements
2
+
3
+ Use this when a "likely" or "hypothesis" finding matters enough to act on, or to get a reliable metric for a before/after comparison. Instrumentation goes into the user's source where there is source to edit, and through `evaluate_script` otherwise.
4
+
5
+ ## Bracket the operation with User Timing
6
+
7
+ Marks and measures appear both as markers in the next captured profile and via `performance.getEntriesByType('measure')`.
8
+
9
+ **If the user's source is available, put the marks there instead.** They then measure only the operation, can wrap code you cannot reach from the outside, survive a navigation, and are still in place for the after-fix capture, so both runs measure the same span.
10
+
11
+ With no source to edit, bracket from outside while the profiler records:
12
+
13
+ 1. `performance.mark('before')`, then perform the STR step with the automation tools.
14
+ 2. `performance.mark('after')` and `performance.measure('op', 'before', 'after')`.
15
+ 3. Read back with `evaluate_script function="() => performance.getEntriesByType('measure').map(m => ({name: m.name, dur: m.duration}))"`, and find the same span in the profile with `thread markers --search op`.
16
+
17
+ Those are three separate tool calls, so the measure also contains the MCP round-trips and your own thinking time between them. Treat it as a way to locate the span in the profile, not as the operation's cost.
18
+
19
+ A measured duration matching the sampled cost confirms the finding; a mismatch means sampling misattributed it, which is common with inlined or minified code.
20
+
21
+ ## LCP and Event Timing
22
+
23
+ Firefox might not support every entry type Chrome does, and `observe()` ignores an unsupported one with a console warning and no exception - you get an empty result that reads like a clean bill of health. Check before relying on one, rather than assuming the Chrome set:
24
+
25
+ `evaluate_script function="() => PerformanceObserver.supportedEntryTypes"`
26
+
27
+ Anything missing from that list has to come from the profile instead.
28
+
29
+ An observer needs a document, and `evaluate_script` runs in the *current* one - a navigation destroys it along with anything on `window`. For a page load, install the observer *after* the navigation settles; `buffered: true` replays entries that already fired, so nothing is missed. For an STR, install it before the action.
30
+
31
+ ```
32
+ evaluate_script function="() => { window.__perf = {lcp: 0, events: []}; new PerformanceObserver(l => { for (const e of l.getEntries()) window.__perf.lcp = e.startTime; }).observe({type: 'largest-contentful-paint', buffered: true}); new PerformanceObserver(l => { for (const e of l.getEntries()) window.__perf.events.push({name: e.name, dur: e.duration}); }).observe({type: 'event', buffered: true, durationThreshold: 16}); }"
33
+ ```
34
+
35
+ Read back with `evaluate_script function="() => window.__perf"`. The last LCP entry wins; `event` entries give per-interaction latency.
@@ -13,7 +13,7 @@ import { tmpdir } from 'node:os';
13
13
 
14
14
  const root = resolve(dirname(fileURLToPath(import.meta.url)), '..');
15
15
  const pkg = JSON.parse(readFileSync(resolve(root, 'package.json'), 'utf8'));
16
- const manifest = JSON.parse(readFileSync(resolve(root, 'manifest.json'), 'utf8'));
16
+ const manifest = JSON.parse(readFileSync(resolve(root, 'manifest.mcpb.json'), 'utf8'));
17
17
 
18
18
  console.log('Building server...');
19
19
  execSync('npm run build', { cwd: root, stdio: 'inherit' });