Files
2026-07-13 13:00:08 +08:00

154 lines
4.3 KiB
Go

package capdiag
import (
"context"
"fmt"
"os/exec"
"strings"
"time"
"reasonix/internal/boot"
"reasonix/internal/config"
"reasonix/internal/plugin"
)
func lookPath(cmd string) (string, error) {
return exec.LookPath(cmd)
}
// probeLiveMCP starts automatic-intent servers in an isolated Host, records
// connection results, and always closes the Host (including stdio children).
// Persistence (startup stats / schema cache) is disabled so --live stays
// free of cache/state side effects under Reasonix home.
func probeLiveMCP(rep *MCPReport, cfg *config.Config, root, home string, timeout time.Duration) []Issue {
var issues []Issue
if cfg == nil {
return issues
}
if timeout <= 0 {
timeout = DefaultLiveTimeout
}
if timeout < MinLiveTimeout {
timeout = MinLiveTimeout
}
if timeout > MaxLiveTimeout {
timeout = MaxLiveTimeout
}
// Only probe automatic start intent servers.
var auto []config.PluginEntry
for _, p := range cfg.Plugins {
if p.ShouldAutoStart() {
auto = append(auto, p)
} else {
for i := range rep.Servers {
if rep.Servers[i].Name == p.Name {
rep.Servers[i].RuntimeStatus = "skipped"
}
}
}
}
if len(auto) == 0 {
return issues
}
specs := boot.PluginSpecsForRoot(auto, root)
ctx, cancel := context.WithTimeout(context.Background(), timeout*time.Duration(len(specs))+timeout)
defer cancel()
host, _, _ := plugin.Start(ctx, specs, plugin.StartPolicy{
PerPluginTimeout: timeout,
Concurrency: 4,
AbortOnError: false,
SkipPersistence: true,
})
if host == nil {
return issues
}
defer host.Close()
byName := map[string]int{}
for i, s := range rep.Servers {
byName[s.Name] = i
}
connected := map[string]bool{}
for _, s := range host.Servers() {
connected[s.Name] = true
tools := make([]MCPToolInfo, 0, len(s.ToolList))
for _, t := range s.ToolList {
tools = append(tools, MCPToolInfo{Name: t.Name, ReadOnlyHint: t.ReadOnlyHint})
}
if i, ok := byName[s.Name]; ok {
rep.Servers[i].RuntimeStatus = "probed"
rep.Servers[i].ToolCount = s.Tools
rep.Servers[i].Tools = tools
} else {
rep.Servers = append(rep.Servers, MCPServerInfo{
Name: s.Name, Transport: s.Transport, RuntimeStatus: "probed",
ToolCount: s.Tools, Tools: tools, StartIntent: "automatic",
})
}
if s.HasTools && s.Tools == 0 {
issues = append(issues, Issue{
Severity: "warning", Code: "mcp.no_tools", Subsystem: "mcp",
Name: s.Name, Message: "live probe: MCP server connected but exposes no tools",
Remediation: "Check server configuration and authentication",
SettingsTab: "mcp",
})
}
}
for _, f := range host.Failures() {
errText := sanitizeErrTextWithPaths(f.Error, root, home)
if i, ok := byName[f.Name]; ok {
rep.Servers[i].RuntimeStatus = "failed"
rep.Servers[i].Error = errText
}
issues = append(issues, Issue{
Severity: "error", Code: "mcp.start_failed", Subsystem: "mcp",
Name: f.Name, Message: "live probe failed: " + errText,
Remediation: "Fix command/URL/auth; re-run with --live after changes",
SettingsTab: "mcp",
})
}
for _, p := range auto {
if connected[p.Name] {
continue
}
if i, ok := byName[p.Name]; ok {
if rep.Servers[i].RuntimeStatus == "failed" {
continue
}
if rep.Servers[i].RuntimeStatus == "" {
rep.Servers[i].RuntimeStatus = "failed"
rep.Servers[i].Error = "no connection result within timeout"
issues = append(issues, Issue{
Severity: "error", Code: "mcp.start_failed", Subsystem: "mcp",
Name: p.Name, Message: fmt.Sprintf("live probe: no result within %s", timeout),
Remediation: "Increase --timeout or fix a hanging MCP server",
SettingsTab: "mcp",
})
}
}
}
return issues
}
// HasErrorSeverity reports whether the report contains any error-level issue.
func HasErrorSeverity(r Report) bool {
for _, is := range r.Issues {
if is.Severity == "error" {
return true
}
}
return false
}
// LiveWarningMessage is printed to stderr before CLI --live starts MCP.
func LiveWarningMessage() string {
return strings.TrimSpace(`warning: --live will start third-party MCP servers in an isolated process.
They may access the network and receive configured environment variables and headers.
Tools are not registered into the agent registry. Startup stats/schema cache writes are disabled.
Host is always closed after the probe.`)
}