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" + } + ] +}