// Package app wires configuration into the running control/data plane. In // Phase 1 it builds the Router from static upstream config; later phases feed // instances from discovery + the reconciler instead. package app import ( "fmt" "log/slog" "os" "gitea.thetempleofdoom.com/drjones/mcp-gateway-nexus/internal/config" "gitea.thetempleofdoom.com/drjones/mcp-gateway-nexus/internal/domain" "gitea.thetempleofdoom.com/drjones/mcp-gateway-nexus/internal/mcp" "gitea.thetempleofdoom.com/drjones/mcp-gateway-nexus/internal/router" "gitea.thetempleofdoom.com/drjones/mcp-gateway-nexus/internal/version" ) // nexusInfo identifies Nexus to upstream MCP servers. func nexusInfo() mcp.Implementation { return mcp.Implementation{Name: "mcp-nexus", Version: version.Version} } // BuildRouter constructs a Router with an upstream client per config entry. // Transports are created but not connected; call ConnectAll afterwards. func BuildRouter(cfg *config.Config, log *slog.Logger) (*router.Router, error) { r := router.New(nexusInfo(), log) for _, u := range cfg.Upstreams { transport, err := buildTransport(u) if err != nil { return nil, fmt.Errorf("upstream %q: %w", u.Namespace, err) } client := mcp.NewClient(transport, nexusInfo()) r.Add(u.Namespace, client) } return r, nil } func buildTransport(u config.UpstreamConfig) (mcp.Transport, error) { switch domain.TransportKind(u.Transport) { case domain.TransportStdio: return mcp.NewStdioTransport(mcp.StdioConfig{ Command: u.Command, Args: u.Args, Env: u.Env, Dir: u.Dir, Stderr: prefixWriter{ns: u.Namespace}, }) case domain.TransportHTTP: return mcp.NewHTTPTransport(mcp.HTTPConfig{ Endpoint: u.Endpoint, Headers: u.Headers, }) default: return nil, fmt.Errorf("unknown transport %q", u.Transport) } } // prefixWriter tags upstream stderr lines with their namespace on our stderr. type prefixWriter struct{ ns string } func (p prefixWriter) Write(b []byte) (int, error) { fmt.Fprintf(os.Stderr, "[upstream:%s] %s", p.ns, b) return len(b), nil }