webctx 0.2.6 → 0.2.7
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 +1 -1
- package/internal/app/app.go +28 -1
- package/internal/app/data_audit_test.go +433 -0
- package/internal/app/data_client.go +193 -5
- package/internal/app/data_contract_constraints.go +65 -0
- package/internal/app/data_examples.go +79 -6
- package/internal/app/data_execute.go +202 -61
- package/internal/app/data_inspect.go +20 -37
- package/internal/app/data_pagination.go +126 -0
- package/internal/app/data_replay.go +83 -0
- package/internal/app/data_research.go +171 -16
- package/internal/app/data_tools_test.go +2 -2
- package/internal/app/data_types.go +35 -12
- package/internal/app/research.go +4 -1
- package/internal/app/research_answers.go +30 -0
- package/internal/app/site_map_page.go +256 -0
- package/internal/app/tools.go +16 -46
- package/package.json +1 -1
- package/pkg/origo/data_test.go +26 -0
- package/pkg/origo/server.go +17 -10
- package/pkg/retrieval/data.go +2 -0
package/internal/app/tools.go
CHANGED
|
@@ -252,57 +252,27 @@ func scrapeLinkWithFirecrawl(rawURL string) (string, string, error) {
|
|
|
252
252
|
}
|
|
253
253
|
|
|
254
254
|
func MapSite(rawURL string) (string, error) {
|
|
255
|
-
|
|
255
|
+
data, err := MapSitePage(MapSiteInput{URL: rawURL})
|
|
256
|
+
if err != nil {
|
|
256
257
|
return "", err
|
|
257
258
|
}
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
259
|
+
parts := []string{
|
|
260
|
+
fmt.Sprintf("total urls found: %v", data["total_discovered"]),
|
|
261
|
+
fmt.Sprintf("matching pages: %v; displayed: %v (offset %v)", data["total_matching"], data["count"], data["offset"]),
|
|
262
|
+
"",
|
|
261
263
|
}
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
"
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
"
|
|
268
|
-
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
|
272
|
-
defer cancel()
|
|
273
|
-
body, err := doJSONRequest(ctx, http.MethodPost, "https://api.firecrawl.dev/v2/map", map[string]string{
|
|
274
|
-
"Authorization": "Bearer " + apiKey,
|
|
275
|
-
"Content-Type": "application/json",
|
|
276
|
-
}, requestBody)
|
|
277
|
-
if err != nil {
|
|
278
|
-
return "", fmt.Errorf("Error mapping website: %v", err)
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
var parsed map[string]any
|
|
282
|
-
if err := json.Unmarshal(body, &parsed); err != nil {
|
|
283
|
-
return "", fmt.Errorf("Error mapping website: %v", err)
|
|
284
|
-
}
|
|
285
|
-
if success, _ := parsed["success"].(bool); !success {
|
|
286
|
-
return "", fmt.Errorf("Error mapping website: Mapping failed for %s: %v", rawURL, parsed["error"])
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
linksAny, _ := parsed["links"].([]any)
|
|
290
|
-
parts := []string{fmt.Sprintf("total urls found: %d", len(linksAny)), ""}
|
|
291
|
-
for _, item := range linksAny {
|
|
292
|
-
switch link := item.(type) {
|
|
293
|
-
case string:
|
|
294
|
-
parts = append(parts, "- "+link, "")
|
|
295
|
-
case map[string]any:
|
|
296
|
-
parts = append(parts, "- "+stringValue(link["url"]))
|
|
297
|
-
if v := stringValue(link["title"]); v != "" {
|
|
298
|
-
parts = append(parts, "- "+v)
|
|
299
|
-
}
|
|
300
|
-
if v := stringValue(link["description"]); v != "" {
|
|
301
|
-
parts = append(parts, "- "+v)
|
|
302
|
-
}
|
|
303
|
-
parts = append(parts, "")
|
|
264
|
+
for _, entry := range data["links"].([]map[string]any) {
|
|
265
|
+
parts = append(parts, "- "+stringField(entry["url"]))
|
|
266
|
+
if title := stringField(entry["title"]); title != "" {
|
|
267
|
+
parts = append(parts, " - "+title)
|
|
268
|
+
}
|
|
269
|
+
if desc := stringField(entry["description"]); desc != "" {
|
|
270
|
+
parts = append(parts, " - "+desc)
|
|
304
271
|
}
|
|
305
272
|
}
|
|
273
|
+
if data["next"] != nil {
|
|
274
|
+
parts = append(parts, "", "More pages available; use --offset with webctx map-site for the next page.")
|
|
275
|
+
}
|
|
306
276
|
return strings.Join(parts, "\n"), nil
|
|
307
277
|
}
|
|
308
278
|
|
package/package.json
CHANGED
package/pkg/origo/data_test.go
CHANGED
|
@@ -66,6 +66,32 @@ func TestDataToolErrorsAreActionableStructuredMCPResults(t *testing.T) {
|
|
|
66
66
|
}
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
+
func TestAuditMCPRejectsZeroLimitAndInvalidSourceURLsWithoutExceptions(t *testing.T) {
|
|
70
|
+
t.Setenv("ORIGO_API_KEY", "secret")
|
|
71
|
+
for _, tt := range []struct {
|
|
72
|
+
name string
|
|
73
|
+
args map[string]any
|
|
74
|
+
code string
|
|
75
|
+
}{
|
|
76
|
+
{"research", map[string]any{"mode": "catalogue", "view": "sources", "limit": 0}, "invalid_limit"},
|
|
77
|
+
{"read_link", map[string]any{"url": "file:///etc/passwd", "question": "summarize"}, "invalid_url"},
|
|
78
|
+
{"map_site", map[string]any{"url": "file:///etc/passwd"}, "invalid_url"},
|
|
79
|
+
} {
|
|
80
|
+
reply := toolResponse(t, tt.name, tt.args, 990)
|
|
81
|
+
if reply["isError"] == true {
|
|
82
|
+
t.Fatalf("%s surfaced a host error: %+v", tt.name, reply)
|
|
83
|
+
}
|
|
84
|
+
data, ok := reply["structuredContent"].(map[string]any)
|
|
85
|
+
if !ok || data["ok"] != false {
|
|
86
|
+
t.Fatalf("%s did not return a structured error: %+v", tt.name, reply)
|
|
87
|
+
}
|
|
88
|
+
failure := data["error"].(map[string]any)
|
|
89
|
+
if failure["code"] != tt.code {
|
|
90
|
+
t.Fatalf("%s expected %s, got %+v", tt.name, tt.code, failure)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
69
95
|
func TestDataToolResearchInspectExecuteOverHTTP(t *testing.T) {
|
|
70
96
|
t.Setenv("ORIGO_API_KEY", "secret")
|
|
71
97
|
t.Setenv("FIRECRAWL_API_KEY", "test")
|
package/pkg/origo/server.go
CHANGED
|
@@ -36,22 +36,21 @@ func makeServer() *mcp.Server {
|
|
|
36
36
|
}, func(_ context.Context, _ *mcp.CallToolRequest, input readLinkInput) (*mcp.CallToolResult, any, error) {
|
|
37
37
|
result, err := retrieval.ReadLinkFocused(input.URL, input.Question)
|
|
38
38
|
if err != nil {
|
|
39
|
-
|
|
39
|
+
code := "read_failed"
|
|
40
|
+
if strings.Contains(err.Error(), "absolute HTTP(S)") || strings.Contains(err.Error(), "readable URL") {
|
|
41
|
+
code = "invalid_url"
|
|
42
|
+
}
|
|
43
|
+
return dataMCPResult(nil, &retrieval.DataError{Code: code, Message: err.Error()})
|
|
40
44
|
}
|
|
41
45
|
return &mcp.CallToolResult{Content: []mcp.Content{&mcp.TextContent{Text: result}}}, nil, nil
|
|
42
46
|
})
|
|
43
|
-
|
|
47
|
+
s.AddTool(&mcp.Tool{
|
|
44
48
|
Name: "map_site",
|
|
45
49
|
Title: "Map site",
|
|
46
|
-
Description: "Discover
|
|
50
|
+
Description: "Discover website pages in bounded, paginated groups (30 by default). Filter by topic, path prefix and language (en by default, all for translations). Returns structured titles, descriptions, counts, and copy-ready next-page arguments.",
|
|
47
51
|
Annotations: &mcp.ToolAnnotations{ReadOnlyHint: true},
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
if err != nil {
|
|
51
|
-
return nil, nil, err
|
|
52
|
-
}
|
|
53
|
-
return &mcp.CallToolResult{Content: []mcp.Content{&mcp.TextContent{Text: result}}}, nil, nil
|
|
54
|
-
})
|
|
52
|
+
InputSchema: mapSiteSchema(),
|
|
53
|
+
}, dataHandler(retrieval.MapSitePage))
|
|
55
54
|
s.AddTool(&mcp.Tool{
|
|
56
55
|
Name: "research",
|
|
57
56
|
Title: "Find data sources",
|
|
@@ -125,6 +124,14 @@ func executeDataSchema() *jsonschema.Schema {
|
|
|
125
124
|
return schema
|
|
126
125
|
}
|
|
127
126
|
|
|
127
|
+
func mapSiteSchema() *jsonschema.Schema {
|
|
128
|
+
schema, err := jsonschema.For[retrieval.MapInput](nil)
|
|
129
|
+
if err != nil {
|
|
130
|
+
panic(err)
|
|
131
|
+
}
|
|
132
|
+
return schema
|
|
133
|
+
}
|
|
134
|
+
|
|
128
135
|
// Manual validation keeps data errors in successful MCP tool responses rather
|
|
129
136
|
// than surfacing them as client-side INVALID_ARGUMENT tool exceptions.
|
|
130
137
|
func dataHandler[In any](fn func(In) (map[string]any, error)) mcp.ToolHandler {
|
package/pkg/retrieval/data.go
CHANGED
|
@@ -7,8 +7,10 @@ type InspectInput = app.InspectDataInput
|
|
|
7
7
|
type ExecuteInput = app.ExecuteDataInput
|
|
8
8
|
type DataCall = app.DataCall
|
|
9
9
|
type DataError = app.DataError
|
|
10
|
+
type MapInput = app.MapSiteInput
|
|
10
11
|
|
|
11
12
|
func Research(input ResearchInput) (map[string]any, error) { return app.ResearchData(input) }
|
|
12
13
|
func Inspect(input InspectInput) (map[string]any, error) { return app.InspectData(input) }
|
|
13
14
|
func Execute(input ExecuteInput) (map[string]any, error) { return app.ExecuteData(input) }
|
|
14
15
|
func DataErrorResult(err error) map[string]any { return app.DataErrorResult(err) }
|
|
16
|
+
func MapSitePage(input MapInput) (map[string]any, error) { return app.MapSitePage(input) }
|