browser-devtools-mcp 0.2.1 → 0.2.3
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.
- package/README.md +202 -28
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/runner.js +158 -0
- package/dist/core-B3VLZZCP.js +1 -0
- package/dist/core-IV5QBQ2N.js +13 -0
- package/dist/core-NLBNZBEB.js +1124 -0
- package/dist/daemon-server.js +1 -1
- package/dist/index.js +2 -209
- package/dist/platform/browser/cli/runner.js +2 -0
- package/dist/platform/browser/index.d.ts +2 -0
- package/dist/platform/browser/tools/a11y/index.d.ts +2 -0
- package/dist/platform/browser/tools/content/index.d.ts +2 -0
- package/dist/platform/browser/tools/debug/index.d.ts +2 -0
- package/dist/platform/browser/tools/figma/index.d.ts +2 -0
- package/dist/platform/browser/tools/index.d.ts +5 -0
- package/dist/platform/browser/tools/interaction/index.d.ts +2 -0
- package/dist/platform/browser/tools/navigation/index.d.ts +2 -0
- package/dist/platform/browser/tools/o11y/index.d.ts +2 -0
- package/dist/platform/browser/tools/react/index.d.ts +2 -0
- package/dist/platform/browser/tools/run/index.d.ts +2 -0
- package/dist/platform/browser/tools/stub/index.d.ts +2 -0
- package/dist/platform/browser/tools/sync/index.d.ts +2 -0
- package/dist/platform/index.d.ts +3 -0
- package/dist/platform/node/cli/runner.js +2 -0
- package/dist/platform/node/entry.js +2 -0
- package/dist/platform/node/index.d.ts +11 -0
- package/dist/platform/node/tools/debug/index.d.ts +6 -0
- package/dist/platform/node/tools/index.d.ts +10 -0
- package/dist/platform/node/tools/run/index.d.ts +2 -0
- package/dist/platform/types.d.ts +15 -0
- package/dist/tools/index.d.ts +1 -4
- package/dist/tools/types.d.ts +8 -3
- package/package.json +6 -2
- package/dist/cli.js +0 -179
- package/dist/core.js +0 -764
- package/dist/tools/a11y/index.d.ts +0 -2
- package/dist/tools/content/index.d.ts +0 -2
- package/dist/tools/debug/index.d.ts +0 -2
- package/dist/tools/figma/index.d.ts +0 -2
- package/dist/tools/interaction/index.d.ts +0 -2
- package/dist/tools/navigation/index.d.ts +0 -2
- package/dist/tools/o11y/index.d.ts +0 -2
- package/dist/tools/react/index.d.ts +0 -2
- package/dist/tools/run/index.d.ts +0 -2
- package/dist/tools/stub/index.d.ts +0 -2
- package/dist/tools/sync/index.d.ts +0 -2
- /package/dist/{otel → platform/browser/otel}/otel-initializer.bundle.js +0 -0
- /package/dist/{tools → platform/browser/tools}/figma/compare/index.d.ts +0 -0
- /package/dist/{tools → platform/browser/tools}/figma/compare/types.d.ts +0 -0
- /package/dist/{types.d.ts → platform/browser/types.d.ts} +0 -0
package/dist/daemon-server.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{
|
|
1
|
+
import{a as h}from"./core-NLBNZBEB.js";import{F as E,G as b,H as C,I as y,K as I,L as u,M as c,O as d,a as t}from"./core-IV5QBQ2N.js";import{createRequire as P}from"node:module";import{Command as N,Option as x,InvalidOptionArgumentError as _}from"commander";import{serve as q}from"@hono/node-server";import{Hono as M}from"hono";import{cors as $}from"hono/cors";import{z as F}from"zod";var k=P(import.meta.url),D=0,A=0,i=new M,a=new Map,O="#default",p={get sessionNotFound(){return f(404,"Session Not Found")},get toolNotFound(){return f(404,"Tool Not Found")},get internalServerError(){return f(500,"Internal Server Error")}};function f(n,r){return{error:{code:n,message:r}}}t(f,"_buildErrorResponse");async function S(n){if(n.closed=!0,n.context)try{await n.context.close(),u("Closed MCP session context")}catch(r){d("Error occurred while closing MCP session context",r)}a.delete(n.id)}t(S,"_closeSession");function H(n,r){let s=Date.now(),e={id:r,toolExecutor:h.toolsInfo.createToolExecutor(()=>r),closed:!1,createdAt:s,lastActiveAt:s};return u(`Created session with id ${r}`),e}t(H,"_createSession");function R(n){let r=Date.now();return{id:n.id,createdAt:n.createdAt,lastActiveAt:n.lastActiveAt,idleSeconds:Math.floor((r-n.lastActiveAt)/1e3)}}t(R,"_getSessionInfo");async function j(n){let r=n.req.header("session-id")||O;return a.get(r)}t(j,"_getSession");async function G(n){let r=n.req.header("session-id")||O,s=a.get(r);return s?u(`Reusing session with id ${r}`):(u(`No session could be found with id ${r}`),s=H(n,r),a.set(r,s)),s}t(G,"_getOrCreateSession");function L(){let n=!1;setInterval(t(()=>{let s=Date.now();n&&a.size===0&&(c("No active session found, so terminating daemon server"),process.exit(0));for(let[e,o]of a)u(`Checking whether session with id ${e} is idle or not ...`),s-o.lastActiveAt>b*1e3&&(u(`Session with id ${e} is idle, so it will be closing ...`),S(o).then(()=>{u(`Session with id ${e} was idle, so it has been closed`)}).catch(l=>{d(`Unable to delete idle session with id ${e}`,l)}));n=a.size===0},"sessionCheck"),C*1e3)}t(L,"_scheduleIdleSessionCheck");async function U(n){let r=n.req.raw.clone();u(`Got request: ${await r.json()}`)}t(U,"_logRequest");async function z(n){let r=Object.fromEntries(h.toolsInfo.tools.map(e=>[e.name(),e]));i.use("*",$({origin:"*",allowMethods:["GET","POST","DELETE","OPTIONS"],allowHeaders:["Content-Type","Authorization","session-id"]})),A=n,D=Date.now();let s=t(async e=>{c(`Received ${e}, initiating graceful shutdown...`);let o=[];for(let l of a.values())o.push(S(l));await Promise.allSettled(o),c("All sessions closed, exiting..."),process.exit(0)},"gracefulShutdown");process.on("SIGTERM",()=>s("SIGTERM")),process.on("SIGINT",()=>s("SIGINT")),process.on("uncaughtException",e=>{d("Uncaught exception",e)}),process.on("unhandledRejection",e=>{d("Unhandled rejection",e)}),i.get("/health",e=>e.json({status:"ok"})),i.get("/info",e=>{let o={version:k("../package.json").version,uptime:Math.floor((Date.now()-D)/1e3),sessionCount:a.size,port:A};return e.json(o)}),i.get("/sessions",e=>{let o=[];for(let l of a.values())o.push(R(l));return e.json({sessions:o})}),i.get("/session",async e=>{let o=await j(e);return o?e.json(R(o)):e.json(p.sessionNotFound,404)}),i.post("/shutdown",async e=>{c("Shutdown request received, closing all sessions...");let o=[];for(let l of a.values())o.push(S(l));return await Promise.allSettled(o),c("All sessions closed, shutting down daemon server..."),setTimeout(()=>{process.exit(0)},500),e.json({status:"shutting_down"},200)}),i.post("/call",async e=>{try{I()&&await U(e);let o=await G(e);o.lastActiveAt=Date.now();let l=await e.req.json(),v=r[l.toolName];if(!v)return e.json(p.toolNotFound,404);let T;try{T=F.object(v.inputSchema()).parse(l.toolInput)}catch(g){let m=g.errors&&Array.isArray(g.errors)?g.errors.map(w=>`${w.path?.join(".")||"input"}: ${w.message}`).join("; "):"Invalid tool input";return e.json(f(400,`Invalid Tool Request: ${m}`),400)}try{let m={toolOutput:await o.toolExecutor.executeTool(v,T)};return e.json(m,200)}catch(g){let m={toolError:{code:g.code,message:g.message}};return e.json(m,500)}}catch(o){return d("Error occurred while handling tool call request",o),e.json(p.internalServerError,500)}}),i.delete("/session",async e=>{try{let o=await j(e);return o?(await S(o),e.json({ok:!0},200)):e.json(p.sessionNotFound,404)}catch(o){return d("Error occurred while deleting session",o),e.json(p.internalServerError,500)}}),i.onError((e,o)=>(d("Unhandled error in request handler",e),o.json({error:{code:500,message:"Internal Server Error"}},500))),i.notFound(e=>e.json({error:"Not Found",status:404},404)),q({fetch:i.fetch,port:n},()=>c(`Listening on port ${n}`)),L()}t(z,"startDaemonHTTPServer");var W=import.meta.url===`file://${process.argv[1]}`||import.meta.url===`file://${process.argv[1]}.mjs`||process.argv[1]?.endsWith("daemon-server.js")||process.argv[1]?.endsWith("daemon-server.mjs");if(W){let n=function(e){let o=Number(e);if(!Number.isInteger(o)||o<1||o>65535)throw new _("port must be an integer between 1 and 65535");return o};Z=n,t(n,"parsePort");let s=new N().addOption(new x("--port <number>","port for daemon HTTP server").argParser(n).default(E)).allowUnknownOption().parse(process.argv).opts();y(),c("Starting daemon HTTP server..."),z(s.port).then(()=>{c("Daemon HTTP server started")}).catch(e=>{d("Failed to start daemon HTTP server",e),process.exit(1)})}var Z;export{z as startDaemonHTTPServer};
|
package/dist/index.js
CHANGED
|
@@ -1,211 +1,4 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import{
|
|
3
|
-
This MCP server exposes a Playwright-powered browser runtime to AI agents,
|
|
4
|
-
enabling deep, bidirectional debugging and interaction with live web pages.
|
|
2
|
+
import{a as d}from"./core-NLBNZBEB.js";import{I as P,J as b,K as y,L as n,M as u,O as c,a as t,b as T,d as v,e as h,f as C}from"./core-IV5QBQ2N.js";import{createRequire as $}from"node:module";var A=$(import.meta.url),M="browser-devtools-mcp",w=A("../package.json").version;function E(){let r=[];return r.push(d.serverInfo.instructions),r.join(`
|
|
5
3
|
|
|
6
|
-
|
|
7
|
-
similar to existing Playwright and Chrome DevTools\u2013based MCP servers, with a focus on AI-driven exploration, diagnosis, and action.
|
|
8
|
-
|
|
9
|
-
Core capabilities include:
|
|
10
|
-
|
|
11
|
-
**Content & Visual Inspection:**
|
|
12
|
-
- Screenshots (full page or specific elements)
|
|
13
|
-
- HTML and text content extraction with filtering options
|
|
14
|
-
- PDF generation with customizable formats
|
|
15
|
-
- Design comparison: Compare live page UI against Figma designs with similarity scoring
|
|
16
|
-
- Accessibility snapshots (ARIA and AX tree) with visual diagnostics
|
|
17
|
-
- Viewport and window resizing for responsive testing
|
|
18
|
-
|
|
19
|
-
**Browser Control & Interaction:**
|
|
20
|
-
- Navigation (go to URL, back, forward)
|
|
21
|
-
- Element interaction (click, fill, hover, select, drag)
|
|
22
|
-
- Keyboard simulation (press-key)
|
|
23
|
-
- Scrolling (viewport or container-based with multiple modes)
|
|
24
|
-
- Viewport emulation and real window resizing
|
|
25
|
-
|
|
26
|
-
**JavaScript Execution:**
|
|
27
|
-
- Run JavaScript in browser page context (access to window, document, DOM, Web APIs)
|
|
28
|
-
- Run JavaScript in Node.js VM sandbox on the server (with Playwright Page access and safe built-ins)
|
|
29
|
-
|
|
30
|
-
**Observability & Monitoring:**
|
|
31
|
-
- Console message capture with filtering
|
|
32
|
-
- HTTP request/response monitoring with detailed filtering
|
|
33
|
-
- Web Vitals performance metrics (LCP, INP, CLS, TTFB, FCP) with recommendations
|
|
34
|
-
- OpenTelemetry trace ID management for distributed tracing correlation
|
|
35
|
-
|
|
36
|
-
**Network Stubbing & Mocking:**
|
|
37
|
-
- HTTP request interception and modification (headers, body, method) using glob patterns
|
|
38
|
-
- HTTP response mocking (fulfill with custom status/headers/body or abort) with configurable delay, times limit, and probability
|
|
39
|
-
- Stub management (list all installed stubs, clear specific or all stubs)
|
|
40
|
-
- Supports A/B testing, security testing, offline testing, error scenarios, and flaky API testing
|
|
41
|
-
|
|
42
|
-
**Synchronization:**
|
|
43
|
-
- Network idle waiting for async operations
|
|
44
|
-
- Configurable timeouts and polling intervals
|
|
45
|
-
|
|
46
|
-
**Design Comparison:**
|
|
47
|
-
- Figma design comparison: Compare live page UI against Figma design snapshots
|
|
48
|
-
- Multi-signal similarity scoring (MSSIM, image embedding, text embedding)
|
|
49
|
-
- Configurable weights and comparison modes (raw vs semantic)
|
|
50
|
-
|
|
51
|
-
**React Component Inspection:**
|
|
52
|
-
- Get component for element: Find React component(s) associated with a DOM element using React Fiber
|
|
53
|
-
- Get element for component: Map a React component instance to the DOM elements it renders
|
|
54
|
-
- Requires persistent browser context (BROWSER_PERSISTENT_ENABLE=true) for optimal operation
|
|
55
|
-
- React DevTools extension must be manually installed in the browser profile (MCP server does NOT auto-install)
|
|
56
|
-
- Without extension, tools fall back to best-effort DOM scanning for __reactFiber$ pointers (less reliable)
|
|
57
|
-
|
|
58
|
-
**Non-Blocking Debugging:**
|
|
59
|
-
- Tracepoints: Capture call stack, local variables, and watch expressions at specific code locations without pausing execution
|
|
60
|
-
- Logpoints: Evaluate and log expressions at code locations (lightweight alternative to tracepoints)
|
|
61
|
-
- Exceptionpoints: Automatically capture snapshots when uncaught or all exceptions occur
|
|
62
|
-
- Netpoints: Monitor specific network requests/responses matching URL patterns
|
|
63
|
-
- Dompoints: Monitor DOM mutations (attribute changes, subtree modifications, node removal) on specific elements
|
|
64
|
-
- Watch expressions: Evaluate custom expressions at every tracepoint hit
|
|
65
|
-
- Source map support: Automatically resolves bundled code locations to original source files
|
|
66
|
-
- Snapshot retrieval: Query captured snapshots by probe ID or sequence number for incremental polling
|
|
67
|
-
|
|
68
|
-
**Advanced Features:**
|
|
69
|
-
- OpenTelemetry integration: Automatic UI trace collection and backend trace correlation
|
|
70
|
-
- Session-based architecture with long-lived browser contexts
|
|
71
|
-
- Persistent browser contexts for stateful sessions (required for React tools)
|
|
72
|
-
- Headless and headful mode support
|
|
73
|
-
- System-installed browser usage option
|
|
74
|
-
- Streamable responses and server-initiated notifications
|
|
75
|
-
- Clean lifecycle management and teardown
|
|
76
|
-
|
|
77
|
-
UI debugging guidance for AI agents:
|
|
78
|
-
- Prefer Accessibility (AX) and ARIA snapshots over raw DOM dumps when diagnosing UI problems.
|
|
79
|
-
These snapshots provide higher-signal, semantically meaningful anchors (roles, names, states) that
|
|
80
|
-
map more reliably to what users perceive and what assistive tech can interact with.
|
|
81
|
-
- Use the AX Tree Snapshot tool to correlate interactive semantics with runtime visual truth:
|
|
82
|
-
bounding boxes, visibility, viewport intersection, and (optionally) computed styles.
|
|
83
|
-
- If a UI control appears present but interactions fail (e.g., clicks do nothing), suspect overlap/occlusion.
|
|
84
|
-
In such cases, enable occlusion checking ("elementFromPoint") to identify which element is actually on top.
|
|
85
|
-
- Use ARIA snapshots to reason about accessibility roles/states and to validate that the intended
|
|
86
|
-
semantics (labels, roles, disabled state, focusability) match the visible UI.
|
|
87
|
-
- Before taking screenshots or snapshots, wait for network idle to ensure page stability.
|
|
88
|
-
- Use Web Vitals tool to assess performance and identify optimization opportunities.
|
|
89
|
-
- For design validation, use "figma_compare-page-with-design" to compare live page UI against Figma designs:
|
|
90
|
-
- Use "semantic" MSSIM mode for comparing real data vs design data (less sensitive to text/value differences)
|
|
91
|
-
- Use "raw" MSSIM mode only when expecting near pixel-identical output
|
|
92
|
-
- If layout mismatch is suspected, run with fullPage=true first, then retry with a selector for the problematic region
|
|
93
|
-
- Notes explain which signals were used or skipped; skipped signals usually mean missing cloud configuration
|
|
94
|
-
- For React component inspection, use "react_get-component-for-element" and "react_get-element-for-component":
|
|
95
|
-
- These tools work best with persistent browser context enabled (BROWSER_PERSISTENT_ENABLE=true)
|
|
96
|
-
- React DevTools extension must be manually installed in the browser profile (MCP server does NOT auto-install)
|
|
97
|
-
- Chrome Web Store: https://chrome.google.com/webstore/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi
|
|
98
|
-
- Without extension, tools use best-effort DOM scanning (less reliable than using DevTools hook)
|
|
99
|
-
- Component names and debug source info are best-effort and may vary by build (dev/prod)
|
|
100
|
-
- For distributed tracing, set trace IDs before navigation to correlate frontend and backend traces.
|
|
101
|
-
- For testing and debugging scenarios, use stub tools to intercept/modify requests or mock responses:
|
|
102
|
-
- Use "stub_intercept-http-request" to modify outgoing requests (inject headers, change body/method)
|
|
103
|
-
- Use "stub_mock-http-response" to mock responses for offline testing, error scenarios, or flaky API simulation
|
|
104
|
-
- Use "stub_list" to check what stubs are active and "stub_clear" to remove them when done
|
|
105
|
-
- For non-blocking JavaScript debugging:
|
|
106
|
-
- Use "debug_put-tracepoint" to capture call stack, local variables, and watch expressions at specific code locations
|
|
107
|
-
- Use "debug_put-logpoint" for lightweight logging at code locations (no call stack capture)
|
|
108
|
-
- Use "debug_put-exceptionpoint" to capture snapshots when exceptions occur (state: "uncaught" or "all")
|
|
109
|
-
- Use "debug_put-netpoint" to monitor specific network requests/responses by URL pattern
|
|
110
|
-
- Use "debug_put-dompoint" to monitor DOM mutations on specific elements
|
|
111
|
-
- Use "debug_add-watch" to add watch expressions evaluated at every tracepoint hit
|
|
112
|
-
- Use "debug_get-tracepoint-snapshots", "debug_get-logpoint-snapshots", etc. to retrieve captured data
|
|
113
|
-
- Use "fromSequence" parameter for incremental polling of new snapshots
|
|
114
|
-
- Tracepoints/logpoints support source maps: set probes on original source file locations
|
|
115
|
-
|
|
116
|
-
This server is designed for AI coding assistants, visual debugging agents, and automated analysis tools
|
|
117
|
-
that need to reason about what a page looks like, how it is structured, and how it behaves \u2014 all through a single MCP interface.
|
|
118
|
-
|
|
119
|
-
It treats the browser as a queryable, inspectable, and controllable execution environment rather than a static screenshot source.
|
|
120
|
-
`,M=`
|
|
121
|
-
<ui_debugging_policy>
|
|
122
|
-
When asked to check for UI problems, layout issues, or visual bugs, ALWAYS follow this policy:
|
|
123
|
-
|
|
124
|
-
1. **Synchronization**: If the page loads content asynchronously, call "sync_wait-for-network-idle" first
|
|
125
|
-
to ensure the page is stable before inspection.
|
|
126
|
-
|
|
127
|
-
2. **Visual Inspection**: Call "content_take-screenshot" for general aesthetics and layout overview.
|
|
128
|
-
|
|
129
|
-
3. **Accessibility Tree Analysis**: Call "a11y_take-ax-tree-snapshot" tool with "checkOcclusion:true"
|
|
130
|
-
- Provides precise bounding boxes, runtime visual data, and occlusion detection
|
|
131
|
-
- Best for detecting overlaps and measuring exact positions
|
|
132
|
-
- Use "onlyVisible:true" or "onlyInViewport:true" to filter results
|
|
133
|
-
- Set "includeStyles:true" to analyze computed CSS properties
|
|
134
|
-
|
|
135
|
-
4. **ARIA Snapshot**: Call "a11y_take-aria-snapshot" tool (full page or specific selector)
|
|
136
|
-
- Provides semantic structure and accessibility roles
|
|
137
|
-
- Best for understanding page hierarchy and accessibility issues
|
|
138
|
-
- Use in combination with AX tree snapshot for comprehensive analysis
|
|
139
|
-
|
|
140
|
-
5. **Design Comparison** (if Figma design is available): Call "figma_compare-page-with-design" tool
|
|
141
|
-
- Compares live page UI against Figma design snapshot
|
|
142
|
-
- Returns combined similarity score using multiple signals (MSSIM, image embedding, text embedding)
|
|
143
|
-
- Use "semantic" mode for real data vs design data comparisons
|
|
144
|
-
- Use "raw" mode only when expecting pixel-identical output
|
|
145
|
-
- Notes explain which signals were used or skipped
|
|
146
|
-
|
|
147
|
-
6. **React Component Inspection** (if page uses React): Use React tools to understand component structure
|
|
148
|
-
- Call "react_get-component-for-element" with selector or (x,y) to find React component for a DOM element
|
|
149
|
-
- Call "react_get-element-for-component" to find DOM elements rendered by a React component
|
|
150
|
-
- **Important:** These tools require persistent browser context (BROWSER_PERSISTENT_ENABLE=true)
|
|
151
|
-
- React DevTools extension must be manually installed in the browser profile for optimal reliability
|
|
152
|
-
- Without extension, tools use best-effort DOM scanning (less reliable)
|
|
153
|
-
- Component names and debug source info are best-effort and may vary by build (dev/prod)
|
|
154
|
-
|
|
155
|
-
7. **Performance Check** (optional but recommended): Call "o11y_get-web-vitals" to assess page performance
|
|
156
|
-
- Identifies performance issues that may affect user experience
|
|
157
|
-
- Provides actionable recommendations based on Google's thresholds
|
|
158
|
-
|
|
159
|
-
8. **Console & Network Inspection**: Check for errors and failed requests
|
|
160
|
-
- Call "o11y_get-console-messages" with "type:ERROR" to find JavaScript errors
|
|
161
|
-
- Call "o11y_get-http-requests" with "ok:false" to find failed network requests
|
|
162
|
-
- If network issues are suspected or testing error scenarios, use stub tools:
|
|
163
|
-
- Use "stub_mock-http-response" to simulate error responses (e.g., 500, 404, timeout) to test UI error handling
|
|
164
|
-
- Use "stub_intercept-http-request" to modify requests (e.g., inject headers) to test different scenarios
|
|
165
|
-
- Use "stub_list" to verify active stubs and "stub_clear" to remove them after testing
|
|
166
|
-
|
|
167
|
-
9. **Manual Verification**: Calculate bounding box overlaps:
|
|
168
|
-
- Horizontal: (element1.x + element1.width) \u2264 element2.x
|
|
169
|
-
- Vertical: (element1.y + element1.height) \u2264 element2.y
|
|
170
|
-
|
|
171
|
-
10. **Report ALL findings**: aesthetic issues, overlaps, spacing problems, alignment issues,
|
|
172
|
-
accessibility problems, semantic structure issues, design parity issues (if compared with Figma),
|
|
173
|
-
React component structure issues (if inspected), performance problems, console errors, failed requests
|
|
174
|
-
|
|
175
|
-
11. **JavaScript Execution** (when needed for advanced debugging):
|
|
176
|
-
- Use "run_js-in-browser" to inspect or mutate DOM state, read client-side variables, or extract computed values directly from the page
|
|
177
|
-
- Use "run_js-in-sandbox" for server-side automation logic that needs access to Playwright Page object or safe built-ins
|
|
178
|
-
|
|
179
|
-
12. **Non-Blocking JavaScript Debugging** (for deep code investigation):
|
|
180
|
-
- Use "debug_put-tracepoint" to set breakpoints that capture call stack and local variables without pausing
|
|
181
|
-
- Use "debug_put-logpoint" for lightweight logging at specific code locations
|
|
182
|
-
- Use "debug_put-exceptionpoint" with state "uncaught" or "all" to capture exception snapshots
|
|
183
|
-
- Use "debug_put-netpoint" to monitor specific network requests by URL pattern
|
|
184
|
-
- Use "debug_put-dompoint" to monitor DOM mutations (attributes, subtree, removal) on elements
|
|
185
|
-
- Use "debug_add-watch" to add expressions evaluated at every tracepoint hit
|
|
186
|
-
- Retrieve snapshots with "debug_get-tracepoint-snapshots", "debug_get-logpoint-snapshots", etc.
|
|
187
|
-
- Use "fromSequence" parameter to poll only new snapshots since last retrieval
|
|
188
|
-
- Probes support source maps: specify original source file paths for bundled applications
|
|
189
|
-
- Use "debug_status" to check current debugging state and probe counts
|
|
190
|
-
|
|
191
|
-
**Tool Usage Notes:**
|
|
192
|
-
- AX tree: Technical measurements, occlusion, precise positioning, visual diagnostics
|
|
193
|
-
- ARIA snapshot: Semantic understanding, accessibility structure, role hierarchy
|
|
194
|
-
- Screenshot: Quick visual reference, but not sufficient alone
|
|
195
|
-
- Network idle: Essential for SPAs and async content
|
|
196
|
-
- Web Vitals: Performance context for UI issues
|
|
197
|
-
- Tracepoints: Deep code investigation with call stack and variables (non-blocking)
|
|
198
|
-
- Logpoints: Lightweight logging at specific code locations
|
|
199
|
-
- Exceptionpoints: Automatic exception capture without manual breakpoints
|
|
200
|
-
- Netpoints/Dompoints: Monitor network and DOM changes without code modification
|
|
201
|
-
|
|
202
|
-
**Important:**
|
|
203
|
-
- Never assume "looks good visually" = "no problems". Overlaps and accessibility issues
|
|
204
|
-
can be functionally broken while appearing visually correct.
|
|
205
|
-
- Always check occlusion when interactions fail or elements appear misaligned.
|
|
206
|
-
- Use scroll tool if elements are below the fold before inspection.
|
|
207
|
-
- For responsive issues, use resize-viewport or resize-window tools to test different sizes.
|
|
208
|
-
</ui_debugging_policy>
|
|
209
|
-
`;function x(){let t=[];return t.push(D),t.join(`
|
|
210
|
-
|
|
211
|
-
`).trim()}s(x,"getServerInstructions");import{StreamableHTTPTransport as L}from"@hono/mcp";import{serve as j}from"@hono/node-server";import{McpServer as $}from"@modelcontextprotocol/sdk/server/mcp.js";import{StdioServerTransport as V}from"@modelcontextprotocol/sdk/server/stdio.js";import{Hono as B}from"hono";import{cors as W}from"hono/cors";var z={jsonrpc:"2.0",error:{code:0,message:"N/A"},id:null},d={get sessionNotFound(){return m(-32001,"Session Not Found")},get unauthorized(){return m(-32001,"Unauthorized")},get internalServerError(){return m(-32603,"Internal Server Error")}},p=new Map;function m(t,e){let o={...z};return o.error.code=t,o.error.message=e,o}s(m,"_buildMCPErrorResponse");function G(t){if("image"in t&&t.image!==null&&typeof t.image=="object"&&"data"in t.image&&"mimeType"in t.image&&Buffer.isBuffer(t.image.data)&&typeof t.image.mimeType=="string"){let e=t.image;return delete t.image,e}}s(G,"_getImage");function J(t){let e=G(t),o=[];return o.push({type:"text",text:JSON.stringify(t,null,2)}),e&&(e.mimeType==="image/svg+xml"?o.push({type:"text",text:e.data.toString(),mimeType:e.mimeType}):o.push({type:"image",data:e.data.toString("base64"),mimeType:e.mimeType})),{content:o,structuredContent:t,isError:!1}}s(J,"_toResponse");function X(t){let e=new $({name:P,version:I},{capabilities:{resources:{},tools:{}},instructions:x()}),o=[];o.push({role:"user",content:{type:"text",text:M}}),e.registerPrompt("default_system",{title:"Default System Prompt",description:"General behavior for the AI assistant"},async()=>({description:"Defines the assistant's general reasoning and tool usage rules.",messages:o}));let n=new T(()=>t.sessionIdProvider?t.sessionIdProvider():""),i=s(a=>async U=>{try{let g=await n.executeTool(a,U);return J(g)}catch(g){return{content:[{type:"text",text:`Error: ${g.message}`}],isError:!0}}},"createToolCallback");return C.forEach(a=>{r(`Registering tool ${a.name()} ...`),e.registerTool(a.name(),{description:a.description(),inputSchema:a.inputSchema(),outputSchema:a.outputSchema()},i(a))}),e}s(X,"_createServer");async function _(t,e){let o=X({config:e.config,sessionIdProvider:s(()=>t.sessionId,"sessionIdProvider")});return await o.connect(t),o}s(_,"_createAndConnectServer");function k(){return{}}s(k,"_getConfig");function Y(t,e,o){let n={transport:e,server:o,closed:!1,lastActiveAt:Date.now()},i=t.env.incoming.socket;return i._mcpRegistered||(i._mcpRegistered=!0,i.on("close",async()=>{r(`Socket, which is for MCP session with id ${e.sessionId}, has been closed`),v&&await e.close()})),ee(e,n.server),r(`Created MCP server session with id ${e.sessionId}`),n}s(Y,"_createSession");async function K(t){let e=k(),o={},n=new L({enableJsonResponse:!0,sessionIdGenerator:s(()=>H.randomUUID(),"sessionIdGenerator"),onsessioninitialized:s(async i=>{let a=Y(t,n,o.server);p.set(i,a),r(`MCP session initialized with id ${i}`)},"onsessioninitialized"),onsessionclosed:s(async i=>{r(`Closing MCP session closed with id ${i} ...`),await n.close(),r(`MCP session closed with id ${i}`)},"onsessionclosed")});return o.server=await _(n,{config:e}),n}s(K,"_createTransport");async function Q(t){let e=t.req.header("mcp-session-id");if(e){let o=p.get(e);if(o)return r(`Reusing MCP session with id ${e}`),o.transport}}s(Q,"_getTransport");async function Z(t){let e=t.req.header("mcp-session-id");if(e){let o=p.get(e);if(o)return r(`Reusing MCP session with id ${e}`),o.transport;r(`No MCP session could be found with id ${e}`);return}return await K(t)}s(Z,"_getOrCreateTransport");function ee(t,e){let o=!1;t.onclose=async()=>{if(r(`Closing MCP session with id ${t.sessionId} ...`),o){r(`MCP session with id ${t.sessionId} has already been closed`);return}o=!0;try{await e.close(),r("Closed MCP server")}catch(n){c("Error occurred while closing MCP server",n)}if(t.sessionId){let n=p.get(t.sessionId);if(n&&(n.closed=!0,n.context))try{await n.context.close(),r("Closed MCP session context")}catch(i){c("Error occurred while closing MCP session context",i)}p.delete(t.sessionId)}r(`Closing MCP session with id ${t.sessionId} ...`)}}s(ee,"_registerMCPSessionClose");function te(){setInterval(s(()=>{let e=Date.now();for(let[o,n]of p)r(`Checking whether session with id ${o} is idle or not ...`),e-n.lastActiveAt>h*1e3&&(r(`Session with id ${o} is idle, so it will be closing ...`),n.transport.close().then(()=>{r(`Session with id ${o} was idle, so it has been closed`)}).catch(i=>{c(`Unable to delete idle session with id ${o}`,i)}))},"sessionCheck"),b*1e3)}s(te,"_scheduleIdleSessionCheck");async function oe(t){let e=t.req.raw.clone();r(`Got request: ${await e.json()}`)}s(oe,"_logRequest");function se(t){let e=t.req.header("mcp-session-id");if(e){let o=p.get(e);o&&(o.lastActiveAt=Date.now())}}s(se,"_markSessionAsActive");async function E(){let t=new V;await _(t,{config:k()})}s(E,"startStdioServer");var l=new B;async function R(t){l.use("*",W({origin:"*",allowMethods:["GET","POST","OPTIONS"],allowHeaders:["Content-Type","Authorization","MCP-Protocol-Version"]})),l.get("/health",e=>e.json({status:"ok"})),l.get("/ping",e=>e.json({status:"ok",message:"pong"})),l.get("/mcp",e=>e.json({status:"ok",protocol:"model-context-protocol",version:"1.0"})),l.post("/mcp",async e=>{try{S()&&await oe(e);let o=await Z(e);return o?(se(e),await o.handleRequest(e)):e.json(d.sessionNotFound,400)}catch(o){return c("Error occurred while handling MCP request",o),e.json(d.internalServerError,500)}}),l.delete("/mcp",async e=>{try{let o=await Q(e);return o?(await o.close(),e.json({ok:!0},200)):e.json(d.sessionNotFound,400)}catch(o){return c("Error occurred while deleting MCP session",o),e.json(d.internalServerError,500)}}),l.notFound(e=>e.json({error:"Not Found",status:404},404)),j({fetch:l.fetch,port:t},()=>u(`Listening on port ${t}`)),te()}s(R,"startStreamableHTTPServer");import{Command as ne,Option as O,InvalidOptionArgumentError as re}from"commander";function ie(t){let e=Number(t);if(!Number.isInteger(e)||e<1||e>65535)throw new re("port must be an integer between 1 and 65535");return e}s(ie,"_parsePort");function ae(){return new ne().addOption(new O("--transport <type>","transport type").choices(["stdio","streamable-http"]).default("stdio")).addOption(new O("--port <number>","port for Streamable HTTP transport").argParser(ie).default(f)).allowUnknownOption().parse(process.argv).opts()}s(ae,"_getOptions");async function ce(){let t=ae();t.transport==="stdio"?(y(),await E()):t.transport==="streamable-http"?(u("Starting MCP server..."),await R(t.port),u("Started MCP Server")):(c(`Invalid transport: ${t.transport}`),process.exit(1))}s(ce,"main");ce().catch(t=>{w(),c("MCP server error",t),process.exit(1)});
|
|
4
|
+
`).trim()}t(E,"getServerInstructions");function I(){return d.serverInfo.policies}t(I,"getServerPolicies");import q from"node:crypto";import{StreamableHTTPTransport as D}from"@hono/mcp";import{serve as V}from"@hono/node-server";import{McpServer as F}from"@modelcontextprotocol/sdk/server/mcp.js";import{StdioServerTransport as L}from"@modelcontextprotocol/sdk/server/stdio.js";import{Hono as z}from"hono";import{cors as U}from"hono/cors";var W={jsonrpc:"2.0",error:{code:0,message:"N/A"},id:null},p={get sessionNotFound(){return S(-32001,"Session Not Found")},get unauthorized(){return S(-32001,"Unauthorized")},get internalServerError(){return S(-32603,"Internal Server Error")}},g=new Map;function S(r,e){let o={...W};return o.error.code=r,o.error.message=e,o}t(S,"_buildMCPErrorResponse");function G(r){if("image"in r&&r.image!==null&&typeof r.image=="object"&&"data"in r.image&&"mimeType"in r.image&&Buffer.isBuffer(r.image.data)&&typeof r.image.mimeType=="string"){let e=r.image;return delete r.image,e}}t(G,"_getImage");function B(r){let e=G(r),o=[];return o.push({type:"text",text:JSON.stringify(r,null,2)}),e&&(e.mimeType==="image/svg+xml"?o.push({type:"text",text:e.data.toString(),mimeType:e.mimeType}):o.push({type:"image",data:e.data.toString("base64"),mimeType:e.mimeType})),{content:o,structuredContent:r,isError:!1}}t(B,"_toResponse");function J(r){let e=new F({name:M,version:w},{capabilities:{resources:{},tools:{}},instructions:E()}),o=[],s=I();if(s)for(let a of s)o.push({role:"user",content:{type:"text",text:a}});e.registerPrompt("default_system",{title:"Default System Prompt",description:"General behavior for the AI assistant"},async()=>({description:"Defines the assistant's general reasoning and tool usage rules.",messages:o}));let i=d.toolsInfo.createToolExecutor(()=>r.sessionIdProvider?r.sessionIdProvider():""),m=t(a=>async H=>{try{let f=await i.executeTool(a,H);return B(f)}catch(f){return{content:[{type:"text",text:`Error: ${f.message}`}],isError:!0}}},"createToolCallback");return d.toolsInfo.tools.forEach(a=>{n(`Registering tool ${a.name()} ...`),e.registerTool(a.name(),{description:a.description(),inputSchema:a.inputSchema(),outputSchema:a.outputSchema()},m(a))}),e}t(J,"_createServer");async function O(r,e){let o=J({config:e.config,sessionIdProvider:t(()=>r.sessionId,"sessionIdProvider")});return await o.connect(r),o}t(O,"_createAndConnectServer");function R(){return{}}t(R,"_getConfig");function K(r,e,o){let s={transport:e,server:o,closed:!1,lastActiveAt:Date.now()},i=r.env.incoming.socket;return i._mcpRegistered||(i._mcpRegistered=!0,i.on("close",async()=>{n(`Socket, which is for MCP session with id ${e.sessionId}, has been closed`),C&&await e.close()})),Z(e,s.server),n(`Created MCP server session with id ${e.sessionId}`),s}t(K,"_createSession");async function Y(r){let e=R(),o={},s=new D({enableJsonResponse:!0,sessionIdGenerator:t(()=>q.randomUUID(),"sessionIdGenerator"),onsessioninitialized:t(async i=>{let m=K(r,s,o.server);g.set(i,m),n(`MCP session initialized with id ${i}`)},"onsessioninitialized"),onsessionclosed:t(async i=>{n(`Closing MCP session closed with id ${i} ...`),await s.close(),n(`MCP session closed with id ${i}`)},"onsessionclosed")});return o.server=await O(s,{config:e}),s}t(Y,"_createTransport");async function Q(r){let e=r.req.header("mcp-session-id");if(e){let o=g.get(e);if(o)return n(`Reusing MCP session with id ${e}`),o.transport}}t(Q,"_getTransport");async function X(r){let e=r.req.header("mcp-session-id");if(e){let o=g.get(e);if(o)return n(`Reusing MCP session with id ${e}`),o.transport;n(`No MCP session could be found with id ${e}`);return}return await Y(r)}t(X,"_getOrCreateTransport");function Z(r,e){let o=!1;r.onclose=async()=>{if(n(`Closing MCP session with id ${r.sessionId} ...`),o){n(`MCP session with id ${r.sessionId} has already been closed`);return}o=!0;try{await e.close(),n("Closed MCP server")}catch(s){c("Error occurred while closing MCP server",s)}if(r.sessionId){let s=g.get(r.sessionId);if(s&&(s.closed=!0,s.context))try{await s.context.close(),n("Closed MCP session context")}catch(i){c("Error occurred while closing MCP session context",i)}g.delete(r.sessionId)}n(`Closing MCP session with id ${r.sessionId} ...`)}}t(Z,"_registerMCPSessionClose");function ee(){setInterval(t(()=>{let e=Date.now();for(let[o,s]of g)n(`Checking whether session with id ${o} is idle or not ...`),e-s.lastActiveAt>v*1e3&&(n(`Session with id ${o} is idle, so it will be closing ...`),s.transport.close().then(()=>{n(`Session with id ${o} was idle, so it has been closed`)}).catch(i=>{c(`Unable to delete idle session with id ${o}`,i)}))},"sessionCheck"),h*1e3)}t(ee,"_scheduleIdleSessionCheck");async function re(r){let e=r.req.raw.clone();n(`Got request: ${await e.json()}`)}t(re,"_logRequest");function oe(r){let e=r.req.header("mcp-session-id");if(e){let o=g.get(e);o&&(o.lastActiveAt=Date.now())}}t(oe,"_markSessionAsActive");async function _(){let r=new L;await O(r,{config:R()})}t(_,"startStdioServer");var l=new z;async function x(r){l.use("*",U({origin:"*",allowMethods:["GET","POST","OPTIONS"],allowHeaders:["Content-Type","Authorization","MCP-Protocol-Version"]})),l.get("/health",e=>e.json({status:"ok"})),l.get("/ping",e=>e.json({status:"ok",message:"pong"})),l.get("/mcp",e=>e.json({status:"ok",protocol:"model-context-protocol",version:"1.0"})),l.post("/mcp",async e=>{try{y()&&await re(e);let o=await X(e);return o?(oe(e),await o.handleRequest(e)):e.json(p.sessionNotFound,400)}catch(o){return c("Error occurred while handling MCP request",o),e.json(p.internalServerError,500)}}),l.delete("/mcp",async e=>{try{let o=await Q(e);return o?(await o.close(),e.json({ok:!0},200)):e.json(p.sessionNotFound,400)}catch(o){return c("Error occurred while deleting MCP session",o),e.json(p.internalServerError,500)}}),l.notFound(e=>e.json({error:"Not Found",status:404},404)),V({fetch:l.fetch,port:r},()=>u(`Listening on port ${r}`)),ee()}t(x,"startStreamableHTTPServer");import{Command as te,Option as N,InvalidOptionArgumentError as se}from"commander";function ne(r){let e=Number(r);if(!Number.isInteger(e)||e<1||e>65535)throw new se("port must be an integer between 1 and 65535");return e}t(ne,"_parsePort");function ie(){return new te().addOption(new N("--transport <type>","transport type").choices(["stdio","streamable-http"]).default("stdio")).addOption(new N("--port <number>","port for Streamable HTTP transport").argParser(ne).default(T)).allowUnknownOption().parse(process.argv).opts()}t(ie,"_getOptions");async function ae(){let r=ie();r.transport==="stdio"?(b(),await _()):r.transport==="streamable-http"?(u("Starting MCP server..."),await x(r.port),u("Started MCP Server")):(c(`Invalid transport: ${r.transport}`),process.exit(1))}t(ae,"main");ae().catch(r=>{P(),c("MCP server error",r),process.exit(1)});
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { Tool } from '../../../tools';
|
|
2
|
+
import { BrowserToolSessionContext } from './context';
|
|
3
|
+
import { BrowserToolExecutor } from './tool-executor';
|
|
4
|
+
export declare const tools: Tool<BrowserToolSessionContext>[];
|
|
5
|
+
export declare function createToolExecutor(sessionIdProvider: () => string): BrowserToolExecutor;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Node.js Platform
|
|
3
|
+
*
|
|
4
|
+
* Exports Node.js platform info for debugging Node.js backend processes.
|
|
5
|
+
*/
|
|
6
|
+
import { PlatformInfo } from '../types';
|
|
7
|
+
export { tools, createToolExecutor } from './tools';
|
|
8
|
+
export { SERVER_INSTRUCTIONS, NODE_DEBUGGING_POLICY } from './server-info';
|
|
9
|
+
export { NodeToolSessionContext } from './tools/context';
|
|
10
|
+
export { NodeToolExecutor } from './tools/tool-executor';
|
|
11
|
+
export declare const nodePlatformInfo: PlatformInfo;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Node.js Platform Tools
|
|
3
|
+
*
|
|
4
|
+
* Aggregates all Node.js debugging tools and provides the tool executor factory.
|
|
5
|
+
*/
|
|
6
|
+
import type { Tool } from '../../../tools';
|
|
7
|
+
import { NodeToolSessionContext } from './context';
|
|
8
|
+
import { NodeToolExecutor } from './tool-executor';
|
|
9
|
+
export declare const tools: Tool<NodeToolSessionContext>[];
|
|
10
|
+
export declare function createToolExecutor(sessionIdProvider: () => string): NodeToolExecutor;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Provider } from '../cli';
|
|
2
|
+
import { Tool, ToolExecutor } from '../tools';
|
|
3
|
+
export type PlatformInfo = {
|
|
4
|
+
serverInfo: {
|
|
5
|
+
instructions: string;
|
|
6
|
+
policies?: string[] | undefined;
|
|
7
|
+
};
|
|
8
|
+
toolsInfo: {
|
|
9
|
+
tools: Tool[];
|
|
10
|
+
createToolExecutor: (sessionIdProvider: () => string) => ToolExecutor;
|
|
11
|
+
};
|
|
12
|
+
cliInfo: {
|
|
13
|
+
cliProvider: Provider;
|
|
14
|
+
};
|
|
15
|
+
};
|
package/dist/tools/index.d.ts
CHANGED
|
@@ -1,4 +1 @@
|
|
|
1
|
-
|
|
2
|
-
export * from './tool-executor';
|
|
3
|
-
export declare const tools: Tool[];
|
|
4
|
-
export type { Tool, ToolInput, ToolOutput, ToolOutputWithImage };
|
|
1
|
+
export type { Tool, ToolExecutor, ToolInput, ToolInputSchema, ToolOutput, ToolOutputSchema, ToolOutputWithImage, ToolSessionContext, } from './types';
|
package/dist/tools/types.d.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { ZodRawShape } from 'zod';
|
|
2
|
-
import { ToolSessionContext } from '../context';
|
|
3
2
|
export type ToolInputSchema = ZodRawShape;
|
|
4
3
|
export type ToolOutputSchema = ZodRawShape;
|
|
5
4
|
export interface ToolInput {
|
|
@@ -12,10 +11,16 @@ export interface ToolOutputWithImage extends ToolOutput {
|
|
|
12
11
|
mimeType: string;
|
|
13
12
|
};
|
|
14
13
|
}
|
|
15
|
-
export interface Tool {
|
|
14
|
+
export interface Tool<SC extends ToolSessionContext = ToolSessionContext> {
|
|
16
15
|
name(): string;
|
|
17
16
|
description(): string;
|
|
18
17
|
inputSchema(): ToolInputSchema;
|
|
19
18
|
outputSchema(): ToolOutputSchema;
|
|
20
|
-
handle(context:
|
|
19
|
+
handle(context: SC, args: ToolInput): Promise<ToolOutput>;
|
|
20
|
+
}
|
|
21
|
+
export interface ToolSessionContext {
|
|
22
|
+
close(): Promise<boolean>;
|
|
23
|
+
}
|
|
24
|
+
export interface ToolExecutor<SC extends ToolSessionContext = ToolSessionContext> {
|
|
25
|
+
executeTool(tool: Tool<SC>, args: ToolInput): Promise<ToolOutput>;
|
|
21
26
|
}
|
package/package.json
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "browser-devtools-mcp",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
4
4
|
"description": "MCP Server for Browser Dev Tools",
|
|
5
5
|
"private": false,
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "./dist/index.js",
|
|
8
8
|
"bin": {
|
|
9
9
|
"browser-devtools-mcp": "./dist/index.js",
|
|
10
|
-
"
|
|
10
|
+
"node-devtools-mcp": "./dist/platform/node/entry.js",
|
|
11
|
+
"browser-devtools-cli": "./dist/platform/browser/cli/runner.js",
|
|
12
|
+
"node-devtools-cli": "./dist/platform/node/cli/runner.js"
|
|
11
13
|
},
|
|
12
14
|
"author": "Serkan Ozal <serkanozal86@gmail.com> (https://github.com/serkan-ozal)",
|
|
13
15
|
"homepage": "https://github.com/serkan-ozal/browser-devtools-mcp",
|
|
@@ -63,6 +65,7 @@
|
|
|
63
65
|
"@types/node": "^18.19.87",
|
|
64
66
|
"@types/picomatch": "^4.0.2",
|
|
65
67
|
"@types/pngjs": "^6.0.5",
|
|
68
|
+
"@types/ws": "^8.18.1",
|
|
66
69
|
"@typescript-eslint/eslint-plugin": "^8.31.1",
|
|
67
70
|
"@typescript-eslint/parser": "^8.31.1",
|
|
68
71
|
"esbuild": "^0.27.2",
|
|
@@ -92,6 +95,7 @@
|
|
|
92
95
|
"pngjs": "^7.0.0",
|
|
93
96
|
"sharp": "^0.34.5",
|
|
94
97
|
"ssim.js": "^3.5.0",
|
|
98
|
+
"ws": "^8.19.0",
|
|
95
99
|
"zod": "^3.24.3"
|
|
96
100
|
}
|
|
97
101
|
}
|