From 956aa3c0cba2a600221dbb4365e1406a39d4da86 Mon Sep 17 00:00:00 2001 From: Lukasz Raczylo Date: Mon, 5 Oct 2026 10:32:22 +0100 Subject: [PATCH] fix(deps): migrate to chromedp 0.19 and drop the cdproto replace Update chromedp to v0.19.1 and cdproto to v0.157.4, port the browser, reader and login code to the generic Action API, group both modules in Renovate, and move the Docker build to Go 1.27. --- Dockerfile | 2 +- go.mod | 15 +++---------- go.sum | 23 ++++---------------- internal/browser/browser.go | 8 +++---- internal/browser/storage.go | 43 ++++++++++++++++--------------------- internal/reader/extract.go | 13 ++++++----- internal/reader/reader.go | 19 +++++++++------- internal/tools/login.go | 24 +++++++++------------ renovate.json | 11 ++++++++++ 9 files changed, 67 insertions(+), 91 deletions(-) create mode 100644 renovate.json diff --git a/Dockerfile b/Dockerfile index 0028b50..add148b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # Build stage -FROM golang:1.26-alpine AS build +FROM golang:1.27-alpine AS build WORKDIR /src COPY go.mod go.sum ./ RUN go mod download diff --git a/go.mod b/go.mod index 7cfb2bf..033d3b6 100644 --- a/go.mod +++ b/go.mod @@ -1,24 +1,18 @@ module github.com/lukaszraczylo/mcp-readitall -go 1.26.4 +go 1.27 require ( github.com/JohannesKaufmann/html-to-markdown/v2 v2.5.2 - github.com/chromedp/cdproto v0.157.2 - github.com/chromedp/chromedp v0.16.0 + github.com/chromedp/cdproto v0.157.4 + github.com/chromedp/chromedp v0.19.1 github.com/modelcontextprotocol/go-sdk v1.8.0 github.com/stretchr/testify v1.12.1 ) require ( github.com/JohannesKaufmann/dom v0.3.1 // indirect - github.com/chromedp/sysutil v1.1.0 // indirect - github.com/go-json-experiment/json v0.0.0-20260820222146-c27c302e5fc3 // indirect - github.com/gobwas/httphead v0.1.0 // indirect - github.com/gobwas/pool v0.2.1 // indirect - github.com/gobwas/ws v1.4.0 // indirect github.com/google/jsonschema-go v0.4.3 // indirect - github.com/orisano/pixelmatch v0.0.0-20230914042517-fa304d1dc785 // indirect github.com/segmentio/asm v1.2.1 // indirect github.com/segmentio/encoding v0.5.4 // indirect github.com/yosida95/uritemplate/v3 v3.0.2 // indirect @@ -29,6 +23,3 @@ require ( golang.org/x/sys v0.48.0 // indirect golang.org/x/time v0.16.0 // indirect ) - -// chromedp v0.16 builds against this cdproto; the v0.157 tag changed the input API. -replace github.com/chromedp/cdproto => github.com/chromedp/cdproto v0.0.0-20260714215040-dc233986426f diff --git a/go.sum b/go.sum index ef06ac8..4134f56 100644 --- a/go.sum +++ b/go.sum @@ -2,32 +2,18 @@ github.com/JohannesKaufmann/dom v0.3.1 h1:J16l9JAHWgkFPR3VIPbQ1gvS0cWab6laK1q7PF github.com/JohannesKaufmann/dom v0.3.1/go.mod h1:BZPkf8ZeYrBgABjwJn9iiKt8aiCtkxpHkevms+Yp2DE= github.com/JohannesKaufmann/html-to-markdown/v2 v2.5.2 h1:XFJZFWESIWlUEHHjzBuv8RvrtCWnSGlimEX17ysSDb8= github.com/JohannesKaufmann/html-to-markdown/v2 v2.5.2/go.mod h1:BHWO8lJzttJLqwuV8Rb1B3OG2OSzLbssZDI1FRg2eAA= -github.com/chromedp/cdproto v0.0.0-20260714215040-dc233986426f h1:0Z1zcSLEmnj2c2CmJYBqewtS6pxhB39bNWUSEUAWjgk= -github.com/chromedp/cdproto v0.0.0-20260714215040-dc233986426f/go.mod h1:RwFsSODCtFExll+GhHM6R92SARHR3Z3oipaxLHj46C0= -github.com/chromedp/chromedp v0.16.0 h1:rOO4deOm4CbZgBCa8mD9g2rDyIoNs0BkgvNrlbp5ouk= -github.com/chromedp/chromedp v0.16.0/go.mod h1:rbuGKFT1vMcFcFqKfPIO1GpX/N+2s8onm2qMxZLbU5U= -github.com/chromedp/sysutil v1.1.0 h1:PUFNv5EcprjqXZD9nJb9b/c9ibAbxiYo4exNWZyipwM= -github.com/chromedp/sysutil v1.1.0/go.mod h1:WiThHUdltqCNKGc4gaU50XgYjwjYIhKWoHGPTUfWTJ8= -github.com/go-json-experiment/json v0.0.0-20260820222146-c27c302e5fc3 h1:UADEEmDKgfXbtnGJZ97beY5XLo9ZechG1nlU4KnRrkE= -github.com/go-json-experiment/json v0.0.0-20260820222146-c27c302e5fc3/go.mod h1:tphK2c80bpPhMOI4v6bIc2xWywPfbqi1Z06+RcrMkDg= -github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU= -github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= -github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og= -github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= -github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs= -github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc= +github.com/chromedp/cdproto v0.157.4 h1:UleIu3RHfaQmhE0ri9GpCbHrGpe0HgV55HVhzhPWglI= +github.com/chromedp/cdproto v0.157.4/go.mod h1:scNPhJ/A4T8BVkyR5k2Uocvoju3iBRZS4cg3uMlxuMk= +github.com/chromedp/chromedp v0.19.1 h1:OAf0ukr4nC5LIwaYEWIv7JYXlpZIZt6ZSDsYXCdjGlc= +github.com/chromedp/chromedp v0.19.1/go.mod h1:4Rs9XtapAMgXxg0s1PpixyQHf37kdnSdu2ov5G4FsZ0= github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY= github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/jsonschema-go v0.4.3 h1:/DBOLZTfDow7pe2GmaJNhltueGTtDKICi8V8p+DQPd0= github.com/google/jsonschema-go v0.4.3/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= -github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80 h1:6Yzfa6GP0rIo/kULo2bwGEkFvCePZ3qHDDTC3/J9Swo= -github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs= github.com/modelcontextprotocol/go-sdk v1.8.0 h1:KIvahhYqwtbeniWVPs3TcXEA7b8jEtwfBpOTAI+Urx4= github.com/modelcontextprotocol/go-sdk v1.8.0/go.mod h1:dL7u98E/zjJTGzEq+j30jQ8K2k1mb6LeAH4inEcSGts= -github.com/orisano/pixelmatch v0.0.0-20230914042517-fa304d1dc785 h1:J1//5K/6QF10cZ59zLcVNFGmBfiSrH8Cho/lNrViK9s= -github.com/orisano/pixelmatch v0.0.0-20230914042517-fa304d1dc785/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/sebdah/goldie/v2 v2.8.0 h1:dZb9wR8q5++oplmEiJT+U/5KyotVD+HNGCAc5gNr8rc= @@ -52,7 +38,6 @@ golang.org/x/oauth2 v0.37.0 h1:JUlcxA8oAtauLfiH8FX2/FkAWHAdi0QtGCGc+hofE98= golang.org/x/oauth2 v0.37.0/go.mod h1:IxwZNxUULJmpBFf9K/9NTMSIfZZuvuTy1gGxhigP/58= golang.org/x/sync v0.23.0 h1:KameEIfc1IkluZyXWLn39Wd4tURc6GbCiISGiZm2bQk= golang.org/x/sync v0.23.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo= golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= golang.org/x/time v0.16.0 h1:vMb6ptszcQMkcwiRTAuNNU50gom6++Q/6gY2hDM6VDE= diff --git a/internal/browser/browser.go b/internal/browser/browser.go index 9666f74..67139bd 100644 --- a/internal/browser/browser.go +++ b/internal/browser/browser.go @@ -66,7 +66,7 @@ func (b *Browser) ensureAllocator() (context.Context, error) { b.allocCtx, b.allocCancel = chromedp.NewExecAllocator(context.Background(), opts...) browserCtx, cancel := chromedp.NewContext(b.allocCtx) - if err := chromedp.Run(browserCtx, chromedp.Navigate("about:blank")); err != nil { + if err := chromedp.Do(browserCtx, chromedp.Navigate("about:blank")); err != nil { b.startErr = fmt.Errorf("warm up browser: %w", err) cancel() return @@ -144,10 +144,8 @@ func (b *Browser) NewContext(parentCtx context.Context, statePath string) (conte // the first step of a fresh login (otherwise stale session cookies from // a previous attempt can mask the new login's success). func ClearAllCookies(ctx context.Context) error { - return chromedp.Run(ctx, chromedp.ActionFunc(func(ctx context.Context) error { - target := chromedp.FromContext(ctx).Target - return network.ClearBrowserCookies().Do(cdp.WithExecutor(ctx, target)) - })) + _, err := chromedp.Call(ctx, network.ClearBrowserCookies, cdp.Empty{}) + return err } // NewHeadedContext spawns a *separate* Chromium process with a visible diff --git a/internal/browser/storage.go b/internal/browser/storage.go index 0917af1..dfc2609 100644 --- a/internal/browser/storage.go +++ b/internal/browser/storage.go @@ -7,7 +7,6 @@ import ( "net/url" "os" "strings" - "time" "github.com/chromedp/cdproto/cdp" "github.com/chromedp/cdproto/network" @@ -76,10 +75,8 @@ func ApplyStorageStateCookies(ctx context.Context, statePath string) error { return nil } - return chromedp.Run(ctx, chromedp.ActionFunc(func(ctx context.Context) error { - target := chromedp.FromContext(ctx).Target - return network.SetCookies(cookies).Do(cdp.WithExecutor(ctx, target)) - })) + _, err = chromedp.Call(ctx, network.SetCookies, network.SetCookiesParams{Cookies: cookies}) + return err } // ApplyLocalStorageOnCurrentOrigin sets every localStorage entry that @@ -99,8 +96,8 @@ func ApplyLocalStorageOnCurrentOrigin(ctx context.Context, statePath string) err return nil } - var currentURL string - if err := chromedp.Run(ctx, chromedp.Location(¤tURL)); err != nil { + currentURL, err := chromedp.Run(ctx, chromedp.Location()) + if err != nil { return fmt.Errorf("read current origin: %w", err) } currentOrigin := normalizeOrigin(currentURL) @@ -123,7 +120,8 @@ func ApplyLocalStorageOnCurrentOrigin(ctx context.Context, statePath string) err return nil } - return chromedp.Run(ctx, chromedp.Evaluate(buildLocalStorageScript(entries), nil)) + _, err = chromedp.Run(ctx, chromedp.Evaluate[chromedp.Void](buildLocalStorageScript(entries))) + return err } // CaptureStorageState reads every cookie set on the current target and @@ -138,8 +136,8 @@ func CaptureStorageState(ctx context.Context, statePath string) error { } // 2) Current origin + localStorage - var currentURL string - if err := chromedp.Run(ctx, chromedp.Location(¤tURL)); err != nil { + currentURL, err := chromedp.Run(ctx, chromedp.Location()) + if err != nil { return fmt.Errorf("read location: %w", err) } origin := normalizeOrigin(currentURL) @@ -182,14 +180,11 @@ func CaptureStorageState(ctx context.Context, statePath string) error { // captureCDPCookies asks the current target for its full cookie list via // the network domain. func captureCDPCookies(ctx context.Context) ([]*network.Cookie, error) { - var out []*network.Cookie - err := chromedp.Run(ctx, chromedp.ActionFunc(func(ctx context.Context) error { - target := chromedp.FromContext(ctx).Target - var err error - out, err = network.GetCookies().Do(cdp.WithExecutor(ctx, target)) - return err - })) - return out, err + res, err := chromedp.Call(ctx, network.GetCookies, network.GetCookiesParams{}) + if err != nil { + return nil, err + } + return res.Cookies, nil } // captureLocalStorage serialises the current page's localStorage to a @@ -200,8 +195,8 @@ func captureLocalStorage(ctx context.Context, origin string) (json.RawMessage, e return nil, nil } const script = `JSON.stringify(Object.fromEntries(Object.keys(localStorage).map(function(k){return [k, localStorage.getItem(k)];})))` - var lsJSON string - if err := chromedp.Run(ctx, chromedp.Evaluate(script, &lsJSON)); err != nil { + lsJSON, err := chromedp.Run(ctx, chromedp.Evaluate[string](script)) + if err != nil { return nil, err } if lsJSON == "" || lsJSON == "[]" || lsJSON == "{}" { @@ -257,9 +252,8 @@ type onDiskCookie struct { } // onDiskToCookieParam converts our on-disk shape into a CDP -// network.CookieParam. Session cookies (Expires <= 0) get a nil Expires -// pointer; persistent cookies get a *cdp.TimeSinceEpoch built from the -// Unix timestamp. +// network.CookieParam. Session cookies (Expires <= 0) keep a zero Expires; +// persistent cookies get a cdp.TimeSinceEpoch built from the Unix timestamp. func onDiskToCookieParam(c onDiskCookie) *network.CookieParam { p := &network.CookieParam{ Name: c.Name, @@ -271,8 +265,7 @@ func onDiskToCookieParam(c onDiskCookie) *network.CookieParam { SameSite: parseSameSite(c.SameSite), } if c.Expires > 0 { - t := cdp.TimeSinceEpoch(time.Unix(int64(c.Expires), 0)) - p.Expires = &t + p.Expires = cdp.TimeSinceEpoch(float64(int64(c.Expires))) } return p } diff --git a/internal/reader/extract.go b/internal/reader/extract.go index 0bf7637..7b4f8d7 100644 --- a/internal/reader/extract.go +++ b/internal/reader/extract.go @@ -4,7 +4,6 @@ import ( "context" "fmt" - "github.com/chromedp/cdproto/cdp" "github.com/chromedp/chromedp" ) @@ -13,8 +12,8 @@ import ( // a separate chromedp.Run so a slow extraction does not re-do navigation. func (r *Reader) extractHTML(ctx context.Context, selector string) (string, error) { if selector == "" { - var html string - if err := chromedp.Run(ctx, chromedp.OuterHTML("html", &html, chromedp.ByQuery)); err != nil { + html, err := chromedp.Run(ctx, chromedp.OuterHTML(chromedp.CSS("html"))) + if err != nil { return "", fmt.Errorf("read document: %w", err) } return html, nil @@ -22,16 +21,16 @@ func (r *Reader) extractHTML(ctx context.Context, selector string) (string, erro // Verify the selector matches at least one node; an empty result is // almost always a caller mistake, not an empty page. - var nodes []*cdp.Node - if err := chromedp.Run(ctx, chromedp.Nodes(selector, &nodes, chromedp.ByQuery)); err != nil { + nodes, err := chromedp.Run(ctx, chromedp.Nodes(chromedp.CSS(selector))) + if err != nil { return "", fmt.Errorf("locate %q: %w", selector, err) } if len(nodes) == 0 { return "", fmt.Errorf("selector %q matched no elements", selector) } - var html string - if err := chromedp.Run(ctx, chromedp.OuterHTML(selector, &html, chromedp.ByQuery)); err != nil { + html, err := chromedp.Run(ctx, chromedp.OuterHTML(chromedp.CSS(selector))) + if err != nil { return "", fmt.Errorf("extract %q: %w", selector, err) } return html, nil diff --git a/internal/reader/reader.go b/internal/reader/reader.go index 8358bc5..44386ce 100644 --- a/internal/reader/reader.go +++ b/internal/reader/reader.go @@ -94,12 +94,12 @@ func (r *Reader) Read(parentCtx context.Context, opts ReadOptions) (*ReadResult, // Build the action chain. Cookies were already applied by NewContext // (if a session exists). localStorage is applied post-navigation // because it is origin-scoped. - actions := []chromedp.Action{ + actions := []chromedp.Action[chromedp.Void]{ chromedp.Navigate(opts.URL), chromedp.WaitReady("body"), } if statePath != "" { - actions = append(actions, chromedp.ActionFunc(func(ctx context.Context) error { + actions = append(actions, chromedp.Func(func(ctx context.Context, _ *chromedp.Target) error { return browser.ApplyLocalStorageOnCurrentOrigin(ctx, statePath) })) } @@ -109,14 +109,17 @@ func (r *Reader) Read(parentCtx context.Context, opts ReadOptions) (*ReadResult, chromedp.Sleep(500*time.Millisecond), ) if opts.WaitSelector != "" { - actions = append(actions, chromedp.WaitVisible(opts.WaitSelector, chromedp.ByQuery)) + actions = append(actions, chromedp.WaitVisible(chromedp.CSS(opts.WaitSelector))) } - actions = append(actions, - chromedp.Title(&title), - chromedp.Location(&finalURL), - ) + actions = append(actions, chromedp.Func(func(ctx context.Context, t *chromedp.Target) (err error) { + if title, err = chromedp.Title()(ctx, t); err != nil { + return err + } + finalURL, err = chromedp.Location()(ctx, t) + return err + })) - if err := chromedp.Run(cdpCtx, actions...); err != nil { + if err := chromedp.Do(cdpCtx, actions...); err != nil { return nil, fmt.Errorf("navigate: %w", err) } diff --git a/internal/tools/login.go b/internal/tools/login.go index f4ca4f4..6ac9452 100644 --- a/internal/tools/login.go +++ b/internal/tools/login.go @@ -8,7 +8,6 @@ import ( "strings" "time" - "github.com/chromedp/cdproto/cdp" "github.com/chromedp/chromedp" "github.com/lukaszraczylo/mcp-readitall/internal/browser" "github.com/modelcontextprotocol/go-sdk/mcp" @@ -78,11 +77,9 @@ func RegisterLogin(s *mcp.Server, pw *browser.Browser, sess *browser.SessionStor return nil, LoginOutput{}, fmt.Errorf("clear cookies: %w", err) } - var title string - if err := chromedp.Run(browserCtx, + if err := chromedp.Do(browserCtx, chromedp.Navigate(in.URL), chromedp.WaitReady("body"), - chromedp.Title(&title), ); err != nil { return nil, LoginOutput{}, fmt.Errorf("navigate: %w", err) } @@ -146,8 +143,8 @@ func waitForLogin(ctx context.Context, pageCtx context.Context, in LoginInput, t check := func() (bool, error) { if in.SuccessURL != "" { - var current string - if err := chromedp.Run(pageCtx, chromedp.Location(¤t)); err != nil { + current, err := chromedp.Run(pageCtx, chromedp.Location()) + if err != nil { return false, err } if strings.Contains(current, in.SuccessURL) { @@ -155,16 +152,15 @@ func waitForLogin(ctx context.Context, pageCtx context.Context, in LoginInput, t } } if in.SuccessSelector != "" { - var nodes []*cdp.Node - if err := chromedp.Run(pageCtx, chromedp.Nodes(in.SuccessSelector, &nodes, chromedp.ByQuery)); err != nil { + nodes, err := chromedp.Run(pageCtx, chromedp.Nodes(chromedp.CSS(in.SuccessSelector))) + if err != nil { return false, err } if len(nodes) > 0 { - var visible bool - if err := chromedp.Run(pageCtx, chromedp.Evaluate( + visible, err := chromedp.Run(pageCtx, chromedp.Evaluate[bool]( `(()=>{const e=document.querySelector(`+jsQuote(in.SuccessSelector)+`);return !!e && e.getBoundingClientRect().height>0;})()`, - &visible, - )); err != nil { + )) + if err != nil { return false, err } if visible { @@ -205,8 +201,8 @@ func waitForLogin(ctx context.Context, pageCtx context.Context, in LoginInput, t // currentLocation returns the page's current URL or "" if the query // fails (e.g. the browser has already been torn down). func currentLocation(ctx context.Context) string { - var s string - if err := chromedp.Run(ctx, chromedp.Location(&s)); err != nil { + s, err := chromedp.Run(ctx, chromedp.Location()) + if err != nil { return "" } return s diff --git a/renovate.json b/renovate.json new file mode 100644 index 0000000..c42c9bb --- /dev/null +++ b/renovate.json @@ -0,0 +1,11 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "packageRules": [ + { + "description": "chromedp pins an exact cdproto; bump them together", + "matchManagers": ["gomod"], + "matchPackageNames": ["github.com/chromedp/chromedp", "github.com/chromedp/cdproto"], + "groupName": "chromedp" + } + ] +}