webctx 0.2.2 → 0.2.4

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.
@@ -0,0 +1,289 @@
1
+ package app
2
+
3
+ import (
4
+ "context"
5
+ "encoding/json"
6
+ "fmt"
7
+ "net/url"
8
+ "sort"
9
+ "strings"
10
+ "time"
11
+ )
12
+
13
+ // API contracts are public documentation, not calls to the documented API.
14
+ // When a question requires exact REST payloads, inspecting the site's linked
15
+ // OpenAPI spec is more faithful than extrapolating HTTP requests from SDK code.
16
+ type openAPIContext struct {
17
+ URL string
18
+ Content string
19
+ }
20
+
21
+ func asksForAPIContracts(question string) bool {
22
+ question = strings.ToLower(question)
23
+ return strings.Contains(question, "rest") || strings.Contains(question, "openapi") ||
24
+ (strings.Contains(question, "api") && (strings.Contains(question, "endpoint") || strings.Contains(question, "request") || strings.Contains(question, "schema") || strings.Contains(question, "contract"))) ||
25
+ (strings.Contains(question, "exact") && (strings.Contains(question, "curl") || strings.Contains(question, "request example")))
26
+ }
27
+
28
+ func discoverOpenAPIContract(rawURL, question string) openAPIContext {
29
+ if !asksForAPIContracts(question) || directSourceLanguage(rawURL) != "" {
30
+ return openAPIContext{}
31
+ }
32
+ u, err := url.Parse(rawURL)
33
+ if err != nil || validateSourceURL(rawURL) != nil || strings.EqualFold(u.Hostname(), "github.com") {
34
+ return openAPIContext{}
35
+ }
36
+ indexURL := u.Scheme + "://" + u.Host + "/llms.txt"
37
+ ctx, cancel := context.WithTimeout(context.Background(), 18*time.Second)
38
+ defer cancel()
39
+ index, err := fetchPublicText(ctx, indexURL, 300000)
40
+ if err != nil {
41
+ return openAPIContext{}
42
+ }
43
+ var specURLs []sourceCandidate
44
+ seen := make(map[string]bool)
45
+ for _, match := range mdLinkPattern.FindAllStringSubmatch(index, 300) {
46
+ dest, err := url.Parse(strings.TrimSpace(match[2]))
47
+ if err != nil {
48
+ continue
49
+ }
50
+ resolved := u.ResolveReference(dest)
51
+ name := strings.ToLower(resolved.Path)
52
+ if !strings.EqualFold(resolved.Hostname(), u.Hostname()) || resolved.Port() != u.Port() ||
53
+ !strings.HasSuffix(name, ".json") || !strings.Contains(name, "openapi") ||
54
+ validateSourceURL(resolved.String()) != nil || sensitiveURL(resolved) {
55
+ continue
56
+ }
57
+ sourceURL := canonicalSourceURL(resolved)
58
+ if seen[sourceURL] {
59
+ continue
60
+ }
61
+ seen[sourceURL] = true
62
+ score := 0
63
+ if strings.Contains(name, "v2-openapi") {
64
+ score += 10
65
+ }
66
+ if strings.HasPrefix(name, "/api-reference/") {
67
+ score += 5
68
+ }
69
+ if name == "/openapi.json" {
70
+ score += 5
71
+ }
72
+ score -= len(name) / 20 // Prefer the canonical path over translations.
73
+ specURLs = append(specURLs, sourceCandidate{url: sourceURL, score: score})
74
+ }
75
+ sort.SliceStable(specURLs, func(i, j int) bool { return specURLs[i].score > specURLs[j].score })
76
+ for i, item := range specURLs {
77
+ if i >= 2 { // Don't recursively fetch every language/version.
78
+ break
79
+ }
80
+ body, err := fetchPublicText(ctx, item.url, maxDirectSourceBytes)
81
+ if err != nil {
82
+ continue
83
+ }
84
+ if content := summarizeOpenAPIContract(body, question); content != "" {
85
+ return openAPIContext{URL: item.url, Content: content}
86
+ }
87
+ }
88
+ return openAPIContext{}
89
+ }
90
+
91
+ type openAPIOperation struct {
92
+ Method string
93
+ Path string
94
+ Summary string
95
+ Score int
96
+ Request map[string]any
97
+ Response map[string]any
98
+ }
99
+
100
+ func summarizeOpenAPIContract(source, question string) string {
101
+ var spec struct {
102
+ OpenAPI string `json:"openapi"`
103
+ Swagger string `json:"swagger"`
104
+ Servers []struct {
105
+ URL string `json:"url"`
106
+ } `json:"servers"`
107
+ Paths map[string]map[string]any `json:"paths"`
108
+ Components struct {
109
+ Schemas map[string]map[string]any `json:"schemas"`
110
+ } `json:"components"`
111
+ }
112
+ if json.Unmarshal([]byte(source), &spec) != nil || (spec.OpenAPI == "" && spec.Swagger == "") || len(spec.Paths) == 0 {
113
+ return ""
114
+ }
115
+ terms := queryTerms(question)
116
+ terms = append(terms, relatedAPITerms(question)...)
117
+ var operations []openAPIOperation
118
+ for path, methods := range spec.Paths {
119
+ // A broad question about Alexandria must not bring in unrelated API
120
+ // routes such as research papers or team activity merely because the
121
+ // path contains "search" or a summary happens to contain "API".
122
+ if strings.Contains(strings.ToLower(question), "alexandria") &&
123
+ strings.Contains(strings.Trim(path, "/"), "/") &&
124
+ !strings.Contains(strings.ToLower(path), "alexandria") {
125
+ continue
126
+ }
127
+ for method, rawDetail := range methods {
128
+ switch strings.ToUpper(method) {
129
+ case "GET", "POST", "PUT", "PATCH", "DELETE":
130
+ default:
131
+ continue
132
+ }
133
+ detail := mapValue(rawDetail)
134
+ summary := stringValue(detail["summary"])
135
+ req := mapValue(detail["requestBody"])
136
+ resp := mapValue(detail["responses"])
137
+ score := termScore(path+" "+summary, terms)*4 + termScore(stringValue(detail["description"]), terms)
138
+ if strings.Contains(strings.ToLower(question), "alexandria") &&
139
+ termScore(strings.Trim(path, "/"), []string{"search", "scrape", "find", "tools", "alexandria"}) == 0 {
140
+ continue
141
+ }
142
+ if examples := requestExamples(req); len(examples) > 0 {
143
+ for name, example := range examples {
144
+ score += termScore(name+" "+stringValue(example["summary"]), terms) * 3
145
+ }
146
+ }
147
+ if score > 0 {
148
+ operations = append(operations, openAPIOperation{
149
+ Method: strings.ToUpper(method), Path: path, Summary: summary, Score: score, Request: req, Response: resp,
150
+ })
151
+ }
152
+ }
153
+ }
154
+ sort.SliceStable(operations, func(i, j int) bool {
155
+ if operations[i].Score == operations[j].Score {
156
+ return operations[i].Path < operations[j].Path
157
+ }
158
+ return operations[i].Score > operations[j].Score
159
+ })
160
+ if len(operations) == 0 {
161
+ return ""
162
+ }
163
+ apiRoot := ""
164
+ for _, server := range spec.Servers {
165
+ if validateSourceURL(server.URL) == nil {
166
+ apiRoot = strings.TrimRight(server.URL, "/")
167
+ break
168
+ }
169
+ }
170
+ parts := []string{"OpenAPI-defined HTTP operations and example request bodies (not executed):"}
171
+ for i, operation := range operations {
172
+ if i >= 4 {
173
+ break
174
+ }
175
+ endpoint := operation.Path
176
+ if apiRoot != "" {
177
+ endpoint = apiRoot + operation.Path
178
+ }
179
+ parts = append(parts, "", fmt.Sprintf("### %s %s", operation.Method, endpoint))
180
+ if operation.Summary != "" {
181
+ parts = append(parts, operation.Summary)
182
+ }
183
+ if examples := requestExamples(operation.Request); len(examples) > 0 {
184
+ names := make([]string, 0, len(examples))
185
+ for name := range examples {
186
+ names = append(names, name)
187
+ }
188
+ sort.SliceStable(names, func(i, j int) bool {
189
+ a := termScore(names[i]+" "+stringValue(examples[names[i]]["summary"]), terms)
190
+ b := termScore(names[j]+" "+stringValue(examples[names[j]]["summary"]), terms)
191
+ if a == b {
192
+ return names[i] < names[j]
193
+ }
194
+ return a > b
195
+ })
196
+ emitted := 0
197
+ for _, name := range names {
198
+ value := examples[name]["value"]
199
+ body, err := json.MarshalIndent(value, "", " ")
200
+ if err != nil || len(body) > 9000 || len(body) == 0 {
201
+ continue
202
+ }
203
+ if strings.Contains(strings.ToLower(question), "alexandria") &&
204
+ !strings.Contains(strings.ToLower(name+" "+string(body)), "alexandria") &&
205
+ !strings.Contains(strings.ToLower(name+" "+string(body)), "findtools") &&
206
+ !strings.Contains(strings.ToLower(name+" "+string(body)), "find-tools") {
207
+ continue
208
+ }
209
+ if emitted >= 2 {
210
+ break
211
+ }
212
+ emitted++
213
+ parts = append(parts, "", "**Published request example:** "+stringValue(examples[name]["summary"]))
214
+ if apiRoot != "" && !strings.Contains(endpoint, "{") {
215
+ // Keep the published JSON body verbatim; the API key must expand in the shell.
216
+ parts = append(parts, fmt.Sprintf("```bash\ncurl -X %s %q \\\n -H \"Authorization: Bearer $API_KEY\" \\\n -H \"Content-Type: application/json\" \\\n --data-binary @- <<'JSON_REQUEST'\n%s\nJSON_REQUEST\n```", operation.Method, endpoint, string(body)))
217
+ } else {
218
+ parts = append(parts, "```json\n"+string(body)+"\n```")
219
+ }
220
+ }
221
+ }
222
+ }
223
+ for _, name := range []string{"FindToolsOptions", "AlexandriaCall", "FindToolsData"} {
224
+ if termScore(question, []string{"alexandria", "tools", "inspect", "schema", "contracts"}) == 0 {
225
+ break
226
+ }
227
+ contract, ok := spec.Components.Schemas[name]
228
+ if !ok {
229
+ continue
230
+ }
231
+ parts = append(parts, "", "### "+name)
232
+ if desc := stringValue(contract["description"]); desc != "" {
233
+ parts = append(parts, desc)
234
+ }
235
+ if props, ok := contract["properties"].(map[string]any); ok {
236
+ keys := make([]string, 0, len(props))
237
+ for key := range props {
238
+ keys = append(keys, key)
239
+ }
240
+ sort.Strings(keys)
241
+ for _, key := range keys {
242
+ item := mapValue(props[key])
243
+ typ := stringValue(item["type"])
244
+ if typ == "" {
245
+ typ = "schema"
246
+ }
247
+ description := stringValue(item["description"])
248
+ if len(description) > 240 {
249
+ description = description[:240] + "…"
250
+ }
251
+ parts = append(parts, fmt.Sprintf("- `%s` (%s): %s", key, typ, description))
252
+ }
253
+ }
254
+ }
255
+ return strings.Join(parts, "\n")
256
+ }
257
+
258
+ func mapValue(value any) map[string]any {
259
+ v, _ := value.(map[string]any)
260
+ return v
261
+ }
262
+
263
+ func requestExamples(request map[string]any) map[string]map[string]any {
264
+ content := mapValue(request["content"])
265
+ jsonContent := mapValue(content["application/json"])
266
+ examples := mapValue(jsonContent["examples"])
267
+ out := make(map[string]map[string]any, len(examples))
268
+ for key, example := range examples {
269
+ if item := mapValue(example); item != nil && item["value"] != nil {
270
+ out[key] = item
271
+ }
272
+ }
273
+ return out
274
+ }
275
+
276
+ func relatedAPITerms(question string) []string {
277
+ question = strings.ToLower(question)
278
+ var terms []string
279
+ if strings.Contains(question, "discover") || strings.Contains(question, "locat") {
280
+ terms = append(terms, "search", "find", "list")
281
+ }
282
+ if strings.Contains(question, "inspect") || strings.Contains(question, "contract") {
283
+ terms = append(terms, "find", "tools", "schema")
284
+ }
285
+ if strings.Contains(question, "execut") || strings.Contains(question, "run ") {
286
+ terms = append(terms, "scrape", "call", "execute")
287
+ }
288
+ return terms
289
+ }
@@ -0,0 +1,140 @@
1
+ package app
2
+
3
+ import (
4
+ "encoding/json"
5
+ "net/http"
6
+ "strings"
7
+ "testing"
8
+ )
9
+
10
+ func exampleOpenAPISpec(t *testing.T) string {
11
+ t.Helper()
12
+ source := map[string]any{
13
+ "openapi": "3.0.0",
14
+ "servers": []any{map[string]any{"url": "https://api.example.com/v2"}},
15
+ "paths": map[string]any{
16
+ "/search": map[string]any{
17
+ "parameters": []any{map[string]any{"name": "unused"}}, // Legal non-operation path entry.
18
+ "post": map[string]any{"summary": "Search the Alexandria catalogue"},
19
+ },
20
+ "/scrape": map[string]any{
21
+ "post": map[string]any{
22
+ "summary": "Execute a provider capability or find tool contracts",
23
+ "requestBody": map[string]any{
24
+ "content": map[string]any{
25
+ "application/json": map[string]any{
26
+ "examples": map[string]any{
27
+ "findTools": map[string]any{
28
+ "summary": "Find Tools (free)",
29
+ "value": map[string]any{
30
+ "alexandria": map[string]any{
31
+ "provider": "firecrawl",
32
+ "capability": "find-tools",
33
+ "options": map[string]any{
34
+ "providers": []string{"particle"},
35
+ "expand": []string{"options", "response"},
36
+ },
37
+ },
38
+ },
39
+ },
40
+ },
41
+ },
42
+ },
43
+ },
44
+ },
45
+ },
46
+ },
47
+ "components": map[string]any{"schemas": map[string]any{
48
+ "FindToolsOptions": map[string]any{
49
+ "description": "Browse providers and inspect tool contracts.",
50
+ "properties": map[string]any{
51
+ "expand": map[string]any{"type": "array", "description": "Contract sections to return"},
52
+ "providers": map[string]any{"type": "array", "description": "Provider IDs"},
53
+ },
54
+ },
55
+ }},
56
+ }
57
+ bytes, err := json.Marshal(source)
58
+ if err != nil {
59
+ t.Fatal(err)
60
+ }
61
+ return string(bytes)
62
+ }
63
+
64
+ func TestOpenAPIContractReturnsExactInspectionRESTExample(t *testing.T) {
65
+ result := summarizeOpenAPIContract(exampleOpenAPISpec(t),
66
+ "What are the Alexandria REST API endpoints for discovering, inspecting, and executing tools? Include exact request examples.")
67
+ for _, want := range []string{
68
+ "POST https://api.example.com/v2/search",
69
+ "POST https://api.example.com/v2/scrape",
70
+ "\"provider\": \"firecrawl\"",
71
+ "\"capability\": \"find-tools\"",
72
+ "\"expand\": [",
73
+ "FindToolsOptions",
74
+ "<<'JSON_REQUEST'",
75
+ "-H \"Authorization: Bearer $API_KEY\"",
76
+ } {
77
+ if !strings.Contains(result, want) {
78
+ t.Errorf("OpenAPI extraction lost %q:\n%s", want, result)
79
+ }
80
+ }
81
+ if count := strings.Count(result, "JSON_REQUEST\n"+strings.Repeat(string(rune(96)), 3)); count != 1 {
82
+ t.Fatalf("expected one complete REST example, got %d:\n%s", count, result)
83
+ }
84
+ }
85
+
86
+ func TestQuestionFocusedReadPullsOfficialOpenAPISpec(t *testing.T) {
87
+ oldClient := http.DefaultClient
88
+ t.Cleanup(func() { http.DefaultClient = oldClient })
89
+ spec := exampleOpenAPISpec(t)
90
+ http.DefaultClient = &http.Client{Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) {
91
+ switch req.URL.Path {
92
+ case "/llms.txt":
93
+ return testHTTPResponse(req, http.StatusOK,
94
+ "# Docs\n\n## OpenAPI Specs\n\n- [v2-openapi](/api-reference/v2-openapi.json)\n", nil), nil
95
+ case "/api-reference/v2-openapi.json":
96
+ return testHTTPResponse(req, http.StatusOK, spec, nil), nil
97
+ default:
98
+ return testHTTPResponse(req, http.StatusNotFound, "", nil), nil
99
+ }
100
+ })}
101
+ primary := "# Alexandria\n\n## Discovery\n\ncurl https://api.example.com/v2/search\n" +
102
+ "\n## Execution\n\ncurl https://api.example.com/v2/scrape\n"
103
+ reader := func(url string) (string, error) { return primary, nil }
104
+ result, err := readLinkFocused("https://docs.example.com/features/alexandria",
105
+ "What are the Alexandria REST API endpoints for discovering, inspecting, and executing tools? Include exact request examples.",
106
+ reader)
107
+ if err != nil {
108
+ t.Fatal(err)
109
+ }
110
+ for _, want := range []string{
111
+ "## REST API contract",
112
+ "**Source:** https://docs.example.com/api-reference/v2-openapi.json",
113
+ "\"capability\": \"find-tools\"",
114
+ "curl https://api.example.com/v2/search",
115
+ "curl https://api.example.com/v2/scrape",
116
+ "**Coverage:** 2 source document(s)",
117
+ } {
118
+ if !strings.Contains(result, want) {
119
+ t.Errorf("focused read missing %q:\n%s", want, result)
120
+ }
121
+ }
122
+ }
123
+
124
+ func TestOpenAPIDiscoveryNeverCrossesToUnrelatedHost(t *testing.T) {
125
+ oldClient := http.DefaultClient
126
+ t.Cleanup(func() { http.DefaultClient = oldClient })
127
+ var calledOutsideIndex bool
128
+ http.DefaultClient = &http.Client{Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) {
129
+ if req.URL.Path != "/llms.txt" {
130
+ calledOutsideIndex = true
131
+ }
132
+ return testHTTPResponse(req, http.StatusOK,
133
+ "- [External OpenAPI](https://evil.example.com/openapi.json)\n"+
134
+ "- [Local OpenAPI](http://127.0.0.1/openapi.json)\n", nil), nil
135
+ })}
136
+ if found := discoverOpenAPIContract("https://docs.example.com/reference",
137
+ "Find exact REST API endpoint request examples"); found.Content != "" || calledOutsideIndex {
138
+ t.Fatalf("unsafe OpenAPI discovery: %+v, off-index=%v", found, calledOutsideIndex)
139
+ }
140
+ }
@@ -0,0 +1,154 @@
1
+ package app
2
+
3
+ import (
4
+ "errors"
5
+ "fmt"
6
+ "net/http"
7
+ "strings"
8
+ "sync"
9
+ "testing"
10
+ )
11
+
12
+ func TestReadLinkFocusedKeepsURLOnlyContract(t *testing.T) {
13
+ called := 0
14
+ reader := func(url string) (string, error) {
15
+ called++
16
+ if url != "https://docs.example.com/api" {
17
+ t.Fatalf("unexpected URL: %s", url)
18
+ }
19
+ return "# Entire document\n\nno focus needed", nil
20
+ }
21
+ got, err := readLinkFocused("https://docs.example.com/api", "", reader)
22
+ if err != nil || got != "# Entire document\n\nno focus needed" || called != 1 {
23
+ t.Fatalf("URL-only path changed: %q, %v, calls=%d", got, err, called)
24
+ }
25
+ }
26
+
27
+ func TestFocusedReadExpandsRelevantSameHostLinks(t *testing.T) {
28
+ old := http.DefaultClient
29
+ t.Cleanup(func() { http.DefaultClient = old })
30
+ http.DefaultClient = &http.Client{Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) {
31
+ return testHTTPResponse(req, http.StatusNotFound, "", nil), nil
32
+ })}
33
+ urls := map[string]string{
34
+ "https://docs.example.com/guide": "# Guide\n[Authentication](./auth)\n[Pagination](/pagination)\n[Offsite](https://evil.example/pagination)\n[Secret](./auth?token=secret)\n[Self](#pagination)",
35
+ "https://docs.example.com/auth": "# Authentication\nSet the Authorization header with your token.",
36
+ "https://docs.example.com/pagination": "# Pagination\nPass next_cursor to read the next page.",
37
+ }
38
+ requested := make(map[string]int)
39
+ var requestedMu sync.Mutex
40
+ reader := func(url string) (string, error) {
41
+ requestedMu.Lock()
42
+ requested[url]++
43
+ requestedMu.Unlock()
44
+ value, ok := urls[url]
45
+ if !ok {
46
+ return "", errors.New("missing URL")
47
+ }
48
+ return value, nil
49
+ }
50
+ got, err := readLinkFocused("https://docs.example.com/guide", "How do authentication and pagination work?", reader)
51
+ if err != nil {
52
+ t.Fatal(err)
53
+ }
54
+ for _, want := range []string{"next_cursor", "Authorization header", "https://docs.example.com/auth", "https://docs.example.com/pagination", "**Coverage:** 3 source document(s)"} {
55
+ if !strings.Contains(got, want) {
56
+ t.Errorf("missing %q in result:\n%s", want, got)
57
+ }
58
+ }
59
+ if len(requested) != 3 {
60
+ t.Fatalf("followed unsafe/irrelevant link: %+v", requested)
61
+ }
62
+ }
63
+
64
+ func TestFocusedExcerptPrioritizesRelevantHeadingAndCode(t *testing.T) {
65
+ large := "# Docs\n" + strings.Repeat("Large unrelated introduction.\n", 600) + "\n## Authenticated requests\nUse an Authorization header.\n```go\nheaders.Set(\"Authorization\", token)\n```\n\n## Other\n" + strings.Repeat("Unrelated text\n", 200)
66
+ got := focusExcerpt(large, "How to authenticate requests with Go?", 3000)
67
+ if !strings.Contains(got, "headers.Set") || strings.Contains(got, "Large unrelated introduction.Large") {
68
+ t.Fatalf("did not select API code example: %s", got)
69
+ }
70
+ if len(got) > 3500 {
71
+ t.Fatalf("excerpt unexpectedly large: %d", len(got))
72
+ }
73
+ }
74
+
75
+ func TestSearchResearchReturnsEvidenceAndSupportingSource(t *testing.T) {
76
+ results := []SearchDoc{
77
+ {URL: "https://docs.example.com/guide", Title: "Guide"},
78
+ {URL: "https://github.com/example/project", Title: "Source"},
79
+ {URL: "https://other.example.com/blog", Title: "Blog"},
80
+ }
81
+ reader := func(url string) (string, error) {
82
+ switch url {
83
+ case "https://docs.example.com/guide":
84
+ return "# Auth\nUse OAuth.\n[Pagination](/pagination)", nil
85
+ case "https://docs.example.com/pagination":
86
+ return "# Pagination\nnext_cursor is optional.", nil
87
+ case "https://github.com/example/project":
88
+ return "# Source\nOAuth settings are configurable.", nil
89
+ case "https://other.example.com/blog":
90
+ return "# Blog\nOAuth authorization examples.", nil
91
+ default:
92
+ return "", fmt.Errorf("unexpected URL: %s", url)
93
+ }
94
+ }
95
+ got := enrichSearchWithReader("OAuth pagination", results, reader)
96
+ if !strings.Contains(got, "Use OAuth.") || !strings.Contains(got, "next_cursor") || !strings.Contains(got, "Supporting reference") {
97
+ t.Fatalf("search lacked fetched source context: %s", got)
98
+ }
99
+ }
100
+
101
+ func TestFollowLinksRejectsOtherHostsSecretsAndAssets(t *testing.T) {
102
+ markdown := "[API auth](https://other.example.com/auth) [API auth](/auth?api_key=private) [API auth](/image.png) [API auth](http://127.0.0.1/auth) [API auth](/auth)"
103
+ got := relatedSourceLinks(markdown, "https://docs.example.com/guide", "api auth", 5)
104
+ if len(got) != 1 || got[0].url != "https://docs.example.com/auth" {
105
+ t.Fatalf("unsafe or irrelevant candidates: %+v", got)
106
+ }
107
+ }
108
+
109
+ func TestRelevanceMatchingDoesNotConfuseAPIWithScraping(t *testing.T) {
110
+ if termScore("advanced scraping guide", []string{"api"}) != 0 {
111
+ t.Fatal("API should not match the middle of the word scraping")
112
+ }
113
+ links := relatedSourceLinks("[Advanced scraping](/advanced-scraping-guide) [API endpoint reference](/api-reference/endpoint/scrape)", "https://docs.example.com/features/alexandria", "Alexandria API endpoints", 3)
114
+ if len(links) != 1 || !strings.Contains(links[0].url, "api-reference") {
115
+ t.Fatalf("irrelevant docs were followed: %+v", links)
116
+ }
117
+ }
118
+
119
+ func TestSearchPrefersTechnicalReferenceOverCommunityPages(t *testing.T) {
120
+ results := []SearchDoc{
121
+ {URL: "https://modelcontextprotocol.io/community/interest-groups/tool-annotations", Title: "Tool annotations interest group"},
122
+ {URL: "https://modelcontextprotocol.io/specification/2025-11-25/server/tools", Title: "Tools specification"},
123
+ {URL: "https://github.com/modelcontextprotocol/go-sdk", Title: "Go SDK for MCP"},
124
+ {URL: "https://pkg.go.dev/golang.org/x/tools/internal/mcp", Title: "Internal Go MCP"},
125
+ }
126
+ selected := chooseSearchSources("Go MCP SDK tool annotations", results, 2)
127
+ if len(selected) != 2 || selected[0].url != "https://github.com/modelcontextprotocol/go-sdk" || !strings.Contains(selected[1].url, "/specification/") {
128
+ t.Fatalf("selected irrelevant pages over official SDK/spec: %+v", selected)
129
+ }
130
+ }
131
+
132
+ func TestFollowLinksSkipGenericMCPLinksForSpecificQuestion(t *testing.T) {
133
+ content := "[Skills over MCP](/community/working-groups/skills-over-mcp) [Tool annotations](/specification/server/tools)"
134
+ links := relatedSourceLinks(content, "https://modelcontextprotocol.io/docs/tools", "Go MCP SDK tool annotations", 2)
135
+ if len(links) != 1 || !strings.Contains(links[0].url, "/specification/server/tools") {
136
+ t.Fatalf("followed generic cross-topic link: %+v", links)
137
+ }
138
+ }
139
+
140
+ func TestSelectJSONFieldsPreservesSyntaxAndPaths(t *testing.T) {
141
+ obj := map[string]any{"paths": map[string]any{"/v2/scrape": map[string]any{"post": map[string]any{"summary": "Scrape an API document"}}}, "metadata": strings.Repeat("x", 5000)}
142
+ got := selectedJSONFields(obj, "scrape", 1400)
143
+ if !strings.Contains(got, "/paths/~1v2~1scrape") || !strings.Contains(got, "```json") || !strings.Contains(got, "Scrape an API document") {
144
+ t.Fatalf("JSON selection missing source pointers: %s", got)
145
+ }
146
+ }
147
+
148
+ func TestLargeOpenAPISchemaUsesSourceJSONPointers(t *testing.T) {
149
+ source := `**URL:** https://docs.example.com/openapi.json` + "\n\n```json\n" + `{"paths":{"/v2/scrape":{"post":{"summary":"Scrape pages"}}},"large":"` + strings.Repeat("x", 12000) + `"}` + "\n```"
150
+ got := focusExcerpt(source, "scrape endpoint", 2500)
151
+ if !strings.Contains(got, "/paths/~1v2~1scrape") || !strings.Contains(got, "Scrape pages") || len(got) > 2700 {
152
+ t.Fatalf("large raw JSON should be selected by pointer: %s", got)
153
+ }
154
+ }
@@ -0,0 +1,106 @@
1
+ package app
2
+
3
+ import (
4
+ "fmt"
5
+ "log"
6
+ "net/url"
7
+ "os"
8
+ "strings"
9
+ "time"
10
+ )
11
+
12
+ // CAPTCHA pages can contain thousands of bytes of translated prompts. Page
13
+ // length is not evidence of useful content, so challenge detection must not
14
+ // be gated on the size of the extracted Markdown.
15
+ func challengeReason(markdown string) string {
16
+ text := strings.ToLower(strings.TrimSpace(markdown))
17
+ if text == "" {
18
+ return "empty source"
19
+ }
20
+ prefix := text[:min(len(text), 4000)]
21
+ for _, marker := range []string{
22
+ "just a moment...",
23
+ "checking your browser",
24
+ "access denied | cloudflare",
25
+ "attention required! | cloudflare",
26
+ "enable javascript and cookies",
27
+ "no content extracted",
28
+ "please complete the security check",
29
+ "verify you are human",
30
+ "verifying you are human",
31
+ "verify that you are human",
32
+ "please verify you are a human",
33
+ "we need to verify your browser",
34
+ "challenge-platform",
35
+ } {
36
+ if strings.Contains(prefix, marker) {
37
+ return "browser verification challenge"
38
+ }
39
+ }
40
+ if strings.Contains(prefix, "hcaptcha") || strings.Contains(prefix, "h-captcha") {
41
+ for _, secondary := range []string{
42
+ "select all", "please verify", "i am human",
43
+ "privacy - terms", "please complete", "security check",
44
+ "captcha challenge", "verify your", "please solve",
45
+ } {
46
+ if strings.Contains(text, secondary) {
47
+ return "hCaptcha challenge"
48
+ }
49
+ }
50
+ }
51
+ if (strings.Count(text, "captcha") > 2 || strings.Count(text, "verify you are human") > 0) &&
52
+ (strings.Contains(text, "try again") || strings.Contains(text, "challenge") || strings.Contains(text, "i am human")) {
53
+ return "CAPTCHA challenge"
54
+ }
55
+ return ""
56
+ }
57
+
58
+ func usableSourceContent(markdown string) bool {
59
+ text := strings.TrimSpace(markdown)
60
+ if len(text) < 10 || challengeReason(text) != "" {
61
+ return false
62
+ }
63
+ // Guard against a 200 status containing an HTML block/error page.
64
+ prefix := strings.ToLower(text[:min(len(text), 350)])
65
+ return !strings.Contains(prefix, "<!doctype html") &&
66
+ !strings.Contains(prefix, "<html")
67
+ }
68
+
69
+ // WEBCTX_DEBUG=1 enables stage traces. Only host + URL path are included;
70
+ // query parameters and credentials are intentionally not logged.
71
+ func traceRetrieval(rawURL, stage, outcome string, start time.Time, err error) {
72
+ if os.Getenv("WEBCTX_DEBUG") != "1" {
73
+ return
74
+ }
75
+ source := "invalid-url"
76
+ if u, parseErr := url.Parse(rawURL); parseErr == nil && u.Hostname() != "" {
77
+ source = u.Hostname() + u.EscapedPath()
78
+ }
79
+ detail := ""
80
+ if err != nil {
81
+ detail = fmt.Sprintf(" error=%q", sanitizedRetrievalError(err))
82
+ }
83
+ log.Printf("webctx retrieval source=%q stage=%q outcome=%q duration_ms=%d%s",
84
+ source, stage, outcome, time.Since(start).Milliseconds(), detail)
85
+ }
86
+
87
+ func sanitizedRetrievalError(err error) string {
88
+ if err == nil {
89
+ return ""
90
+ }
91
+ message := strings.ToLower(err.Error())
92
+ switch {
93
+ case strings.Contains(message, "403"), strings.Contains(message, "forbidden"):
94
+ return "forbidden"
95
+ case strings.Contains(message, "429"), strings.Contains(message, "rate limit"):
96
+ return "rate-limited"
97
+ case strings.Contains(message, "timeout"), strings.Contains(message, "deadline"):
98
+ return "timeout"
99
+ case strings.Contains(message, "captcha"), strings.Contains(message, "challenge"):
100
+ return "challenge"
101
+ case strings.Contains(message, "404"), strings.Contains(message, "not found"):
102
+ return "not-found"
103
+ default:
104
+ return "source-unavailable"
105
+ }
106
+ }