Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 49 additions & 6 deletions cmd/scout/mcp.go
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,29 @@ type NetworkRequestsResult struct {
Hint string `json:"hint,omitempty"`
}

// FailedRequestsResult wraps the failed_requests list in a JSON object so the
// tool can advertise a fixed output schema (structuredContent must be an object,
// not a bare array). Each element is a fixed-shape agent.NetworkFailure.
type FailedRequestsResult struct {
Items []agent.NetworkFailure `json:"items"`
Total int `json:"total"`
}

// ListTabsResult wraps the list_tabs array in a JSON object envelope so the tool
// can advertise a fixed output schema. Each element is a fixed-shape agent.TabInfo.
type ListTabsResult struct {
Items []agent.TabInfo `json:"items"`
Total int `json:"total"`
}

// CookiesListResult wraps the cookies_list array in a JSON object envelope so the
// tool can advertise a fixed output schema. Each element is a fixed-shape
// agent.CookieInfo (values redacted).
type CookiesListResult struct {
Items []agent.CookieInfo `json:"items"`
Total int `json:"total"`
}

type NetworkSummaryInput struct {
Pattern string `json:"pattern,omitempty" jsonschema:"description=Optional URL substring filter."`
}
Expand Down Expand Up @@ -964,6 +987,7 @@ WORKFLOW: navigate first, then use other tools. Use 'dismiss_cookies' after navi

srv.Tool("network_requests").
ReadOnly().
OutputSchema(NetworkRequestsResult{}).
Description("Get captured network requests/responses including request bodies (POST/PUT/PATCH) and response bodies (max 32KB each, truncated if larger). Includes recent buffered requests so you can inspect traffic even if capture was enabled late. If the list is empty and capture was never enabled, the response includes a hint pointing at enable_network_capture.").
Handler(func(ctx context.Context, input NetworkRequestsInput) (*NetworkRequestsResult, error) {
out := s().CapturedRequests(input.Pattern)
Expand Down Expand Up @@ -1044,9 +1068,14 @@ WORKFLOW: navigate first, then use other tools. Use 'dismiss_cookies' after navi

srv.Tool("cookies_list").
ReadOnly().
OutputSchema(CookiesListResult{}).
Description("List all cookies for the active page (names + flags only — values are redacted). Useful for diagnosing stale-session issues after a backend restart.").
Handler(func(ctx context.Context, input ObserveInput) ([]agent.CookieInfo, error) {
return s().ListCookies()
Handler(func(ctx context.Context, input ObserveInput) (*CookiesListResult, error) {
cookies, err := s().ListCookies()
if err != nil {
return nil, err
}
return &CookiesListResult{Items: cookies, Total: len(cookies)}, nil
})

srv.Tool("cookies_clear").
Expand Down Expand Up @@ -1074,13 +1103,15 @@ WORKFLOW: navigate first, then use other tools. Use 'dismiss_cookies' after navi

srv.Tool("check_readiness").
ReadOnly().
OutputSchema(agent.PageReadiness{}).
Description("Check how ready the page is for interaction. Returns a 0-100 score, pending XHR count, skeleton/spinner presence, and suggestions for what to wait for.").
Handler(func(ctx context.Context, input ObserveInput) (*agent.PageReadiness, error) {
return s().CheckReadiness()
})

srv.Tool("web_vitals").
ReadOnly().
OutputSchema(agent.WebVitalsResult{}).
Description("Extract Core Web Vitals (LCP, CLS, INP) and performance timing (TTFB, First Paint, DOM Content Loaded). Each metric is rated good/needs-improvement/poor per Google thresholds.").
Handler(func(ctx context.Context, input ObserveInput) (*agent.WebVitalsResult, error) {
return s().WebVitals()
Expand Down Expand Up @@ -1159,16 +1190,22 @@ WORKFLOW: navigate first, then use other tools. Use 'dismiss_cookies' after navi

srv.Tool("console_errors").
ReadOnly().
OutputSchema(agent.DiagnosticsResult{}).
Description("Get captured console.error / console.warn messages plus recent network 4xx/5xx failures. Auto-installs lightweight network observers, so failures recorded after the first call surface here without an explicit enable_network_capture.").
Handler(func(ctx context.Context, input ObserveInput) (*agent.DiagnosticsResult, error) {
return s().Diagnostics()
})

srv.Tool("failed_requests").
ReadOnly().
OutputSchema(FailedRequestsResult{}).
Description("Recent network requests with status >= 400 (4xx/5xx). URL + method + status + response body snippet. Use after a form submission that silently failed.").
Handler(func(ctx context.Context, input ObserveInput) ([]agent.NetworkFailure, error) {
return s().FailedRequests()
Handler(func(ctx context.Context, input ObserveInput) (*FailedRequestsResult, error) {
failures, err := s().FailedRequests()
if err != nil {
return nil, err
}
return &FailedRequestsResult{Items: failures, Total: len(failures)}, nil
})

srv.Tool("detect_auth_wall").
Expand Down Expand Up @@ -1233,6 +1270,7 @@ WORKFLOW: navigate first, then use other tools. Use 'dismiss_cookies' after navi

srv.Tool("discover_form").
ReadOnly().
OutputSchema(agent.FormDiscoveryResult{}).
Description("Discover form fields with their labels, types, and CSS selectors.").
Handler(func(ctx context.Context, input DiscoverFormInput) (*agent.FormDiscoveryResult, error) {
return s().DiscoverForm(input.Selector)
Expand Down Expand Up @@ -1266,9 +1304,14 @@ WORKFLOW: navigate first, then use other tools. Use 'dismiss_cookies' after navi

srv.Tool("list_tabs").
ReadOnly().
OutputSchema(ListTabsResult{}).
Description("List all open tabs with their names, URLs, and titles.").
Handler(func(ctx context.Context, input ObserveInput) ([]agent.TabInfo, error) {
return s().ListTabs()
Handler(func(ctx context.Context, input ObserveInput) (*ListTabsResult, error) {
tabs, err := s().ListTabs()
if err != nil {
return nil, err
}
return &ListTabsResult{Items: tabs, Total: len(tabs)}, nil
})

// --- Frames ---
Expand Down
50 changes: 50 additions & 0 deletions cmd/scout/mcp_outputschema_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package main

import (
"testing"

"go.klarlabs.de/mcp/schema"
"go.klarlabs.de/scout/agent"
)

// TestOutputSchemasGenerate guards every type advertised via ToolBuilder.OutputSchema.
//
// OutputSchema runs schema.Generate at registration time; if it errors the
// ToolBuilder silently drops the tool. This test fails loudly instead, so a
// type that becomes unrepresentable (e.g. an added field with no JSON schema
// mapping) is caught in CI rather than silently removing a production tool.
func TestOutputSchemasGenerate(t *testing.T) {
cases := []struct {
name string
val any
}{
// Newly advertised data-tool output types.
{"network_requests", NetworkRequestsResult{}},
{"web_vitals", agent.WebVitalsResult{}},
{"console_errors", agent.DiagnosticsResult{}},
{"check_readiness", agent.PageReadiness{}},
{"discover_form", agent.FormDiscoveryResult{}},
{"failed_requests", FailedRequestsResult{}},
{"list_tabs", ListTabsResult{}},
{"cookies_list", CookiesListResult{}},
// Pre-existing advertised types — kept here so the guard covers the
// full set of production output schemas.
{"observe", agent.Observation{}},
{"submit_outcome", agent.SubmitOutcome{}},
{"wait_for_navigation", agent.NavigationOutcome{}},
{"aria_violations", agent.AriaViolationReport{}},
{"network_summary", agent.NetworkSummary{}},
}

for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
s, err := schema.Generate(tc.val)
if err != nil {
t.Fatalf("schema.Generate(%T) failed: %v", tc.val, err)
}
if s == nil {
t.Fatalf("schema.Generate(%T) returned nil schema", tc.val)
}
})
}
}
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ require (
github.com/gorilla/websocket v1.5.3
go.klarlabs.de/bolt v1.5.2
go.klarlabs.de/fortify v1.8.1
go.klarlabs.de/mcp v1.21.0
go.klarlabs.de/mcp v1.22.0
go.klarlabs.de/statekit v1.8.0
)

Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ go.klarlabs.de/bolt v1.5.2 h1:HB5p1tCZdizz7JpTSZYFPtp6R9oJB1LLVq74M9vH88w=
go.klarlabs.de/bolt v1.5.2/go.mod h1:wz+vynyuDkLK2fgmoi0VvLurqvq4JcnMTMyDQpW+f7s=
go.klarlabs.de/fortify v1.8.1 h1:gyzJ7ESNe/Qf65DhGCT1WPWDyphgEDApb2QTx3Gd2m0=
go.klarlabs.de/fortify v1.8.1/go.mod h1:x4Rnk8xt8ckJ8Pxe5SsXZVfGYjJIoOHx67DBl5Ncis8=
go.klarlabs.de/mcp v1.21.0 h1:SFf0Cr2rPYhLw0VnW1eec7y77vFddfEGXsPCv1ixEWs=
go.klarlabs.de/mcp v1.21.0/go.mod h1:ZeezqVm3aJFDN2+a7W0BVrLcEDhD2tdA+tQb+Klv5FE=
go.klarlabs.de/mcp v1.22.0 h1:2ufVDqJT2/+kt5zd8B9HjbxJUbz3DYEVe43PqwTQ/QI=
go.klarlabs.de/mcp v1.22.0/go.mod h1:ZeezqVm3aJFDN2+a7W0BVrLcEDhD2tdA+tQb+Klv5FE=
go.klarlabs.de/statekit v1.8.0 h1:bOTAuextbB46f2tzXs9zpZ5qdwLIzE1e8OE5eDPv6fM=
go.klarlabs.de/statekit v1.8.0/go.mod h1:ZYFkCAGSdRm/jx7QDvVqWsGwRZGc8ndhE7dk7NtsE7E=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
Expand Down