mirror of
https://github.com/lukaszraczylo/mcp-readitall.git
synced 2026-10-12 00:45:33 +00:00
97 lines
3.3 KiB
Go
97 lines
3.3 KiB
Go
// Package tools registers each MCP tool with the server. One file per tool
|
|
// keeps the schemas, handlers, and tool registration together so changes are
|
|
// easy to audit.
|
|
package tools
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/lukaszraczylo/mcp-readitall/internal/reader"
|
|
"github.com/modelcontextprotocol/go-sdk/mcp"
|
|
)
|
|
|
|
// ReadInput is the JSON schema for the read_url tool.
|
|
type ReadInput struct {
|
|
URL string `json:"url" jsonschema:"Absolute http(s) URL to read"`
|
|
Selector string `json:"selector,omitempty" jsonschema:"Optional CSS selector to extract only a region of the page"`
|
|
WaitFor string `json:"wait_for,omitempty" jsonschema:"Optional CSS selector to wait for before extracting (useful for SPAs that hydrate after networkidle)"`
|
|
TimeoutSeconds int `json:"timeout_seconds,omitempty" jsonschema:"Overall timeout in seconds (1-300, default 30)"`
|
|
MaxChars int `json:"max_chars,omitempty" jsonschema:"Truncate returned markdown to roughly this many characters (100-500000)"`
|
|
}
|
|
|
|
// ReadOutput is returned to the MCP client. The Markdown field is the primary
|
|
// payload; other fields give the model enough context to decide what to do
|
|
// next.
|
|
type ReadOutput struct {
|
|
URL string `json:"url"`
|
|
FinalURL string `json:"final_url"`
|
|
Title string `json:"title"`
|
|
Markdown string `json:"markdown"`
|
|
UsedSession bool `json:"used_session"`
|
|
Truncated bool `json:"truncated"`
|
|
Bytes int `json:"bytes"`
|
|
}
|
|
|
|
// ReadResult is the text-side companion of ReadOutput, surfaced via the
|
|
// CallToolResult's Content for clients that ignore structured content.
|
|
func readText(o ReadOutput) string {
|
|
var b strings.Builder
|
|
b.WriteString("# ")
|
|
b.WriteString(o.Title)
|
|
if o.Title == "" {
|
|
b.WriteString(o.URL)
|
|
}
|
|
b.WriteString("\n\n")
|
|
b.WriteString(o.Markdown)
|
|
if o.Truncated {
|
|
b.WriteString("\n\n[truncated]")
|
|
}
|
|
if o.FinalURL != "" && o.FinalURL != o.URL {
|
|
b.WriteString("\n\n_Resolved to: ")
|
|
b.WriteString(o.FinalURL)
|
|
b.WriteString("_")
|
|
}
|
|
if o.UsedSession {
|
|
b.WriteString("\n\n_Used saved session._")
|
|
}
|
|
return b.String()
|
|
}
|
|
|
|
// RegisterRead wires the read_url tool to the server.
|
|
func RegisterRead(s *mcp.Server, r *reader.Reader) {
|
|
mcp.AddTool(s, &mcp.Tool{
|
|
Name: "read_url",
|
|
Description: "Fetch a URL through a headless browser (full SPA support), render the page, and return its content as Markdown. If a session has previously been saved for the URL's host via the login tool, it is used automatically so authenticated pages render as the logged-in user.",
|
|
}, func(ctx context.Context, _ *mcp.CallToolRequest, in ReadInput) (*mcp.CallToolResult, ReadOutput, error) {
|
|
if in.URL == "" {
|
|
return nil, ReadOutput{}, fmt.Errorf("url is required")
|
|
}
|
|
timeout := time.Duration(in.TimeoutSeconds) * time.Second
|
|
res, err := r.Read(ctx, reader.ReadOptions{
|
|
URL: in.URL,
|
|
Selector: in.Selector,
|
|
WaitSelector: in.WaitFor,
|
|
Timeout: timeout,
|
|
MaxChars: in.MaxChars,
|
|
})
|
|
if err != nil {
|
|
return nil, ReadOutput{}, err
|
|
}
|
|
out := ReadOutput{
|
|
URL: res.URL,
|
|
FinalURL: res.FinalURL,
|
|
Title: res.Title,
|
|
Markdown: res.Markdown,
|
|
UsedSession: res.UsedSession,
|
|
Truncated: res.Truncated,
|
|
Bytes: res.Bytes,
|
|
}
|
|
return &mcp.CallToolResult{
|
|
Content: []mcp.Content{&mcp.TextContent{Text: readText(out)}},
|
|
}, out, nil
|
|
})
|
|
}
|