diff --git a/internal/config/config.go b/internal/config/config.go index fa70b49..2a4e987 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -3,6 +3,7 @@ package config import ( "encoding/json" "fmt" + "net" "os" "regexp" "strconv" @@ -13,6 +14,7 @@ import ( ) const agentConfigPath = "/agyn-bin/config.json" +const defaultMCPHost = "127.0.0.1" var mcpServerNamePattern = regexp.MustCompile(`^[a-z][a-z0-9_]*$`) @@ -36,6 +38,7 @@ type Config struct { SDK string AgentBinary string WorkDir string + MCPHost string MCPServers []MCPServer } @@ -66,6 +69,18 @@ func fromEnv(configPath string) (Config, error) { llmAPIToken = "platform" } + mcpHost := strings.TrimSpace(os.Getenv("AGENT_MCP_HOST")) + if mcpHost == "" { + mcpHost = defaultMCPHost + } else { + if strings.Contains(mcpHost, "://") || strings.Contains(mcpHost, "/") { + return Config{}, fmt.Errorf("AGENT_MCP_HOST must be a hostname without scheme or path") + } + if host, port, err := net.SplitHostPort(mcpHost); err == nil && host != "" && port != "" { + return Config{}, fmt.Errorf("AGENT_MCP_HOST must not include a port") + } + } + mcpServers, err := parseMCPServers(os.Getenv("AGENT_MCP_SERVERS")) if err != nil { return Config{}, err @@ -99,6 +114,7 @@ func fromEnv(configPath string) (Config, error) { SDK: sdk, AgentBinary: agentBinary, WorkDir: workDir, + MCPHost: mcpHost, MCPServers: mcpServers, }, nil } diff --git a/internal/config/config_test.go b/internal/config/config_test.go index f5ff6e2..76715e2 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -40,6 +40,7 @@ func TestFromEnvValid(t *testing.T) { t.Setenv("THREAD_ID", "thread-123") t.Setenv("LLM_BASE_URL", "https://llm.example") t.Setenv("LLM_API_TOKEN", "token-123") + t.Setenv("AGENT_MCP_HOST", "mcp.internal") cfg, err := fromEnv(configPath) if err != nil { @@ -72,6 +73,9 @@ func TestFromEnvValid(t *testing.T) { if cfg.WorkDir != "/tmp/workdir" { t.Fatalf("unexpected work dir: %s", cfg.WorkDir) } + if cfg.MCPHost != "mcp.internal" { + t.Fatalf("unexpected MCP host: %s", cfg.MCPHost) + } } func TestFromEnvLLMAPITokenDefault(t *testing.T) { @@ -126,6 +130,37 @@ func TestFromEnvDefaults(t *testing.T) { } } +func TestFromEnvMCPHostDefault(t *testing.T) { + setRequiredEnv(t) + configPath := writeAgentConfig(t, "codex", "/opt/bin/codex") + t.Setenv("AGENT_MCP_HOST", "") + + cfg, err := fromEnv(configPath) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + if cfg.MCPHost != "127.0.0.1" { + t.Fatalf("expected default MCP host, got %s", cfg.MCPHost) + } +} + +func TestFromEnvMCPHostInvalid(t *testing.T) { + setRequiredEnv(t) + configPath := writeAgentConfig(t, "codex", "/opt/bin/codex") + + t.Setenv("AGENT_MCP_HOST", "http://localhost") + _, err := fromEnv(configPath) + if err == nil { + t.Fatalf("expected error for AGENT_MCP_HOST with scheme") + } + + t.Setenv("AGENT_MCP_HOST", "localhost:8100") + _, err = fromEnv(configPath) + if err == nil { + t.Fatalf("expected error for AGENT_MCP_HOST with port") + } +} + func TestFromEnvGatewayDefault(t *testing.T) { setRequiredEnv(t) configPath := writeAgentConfig(t, "codex", "/opt/bin/codex") diff --git a/internal/daemon/agn.go b/internal/daemon/agn.go index 73441c4..97551af 100644 --- a/internal/daemon/agn.go +++ b/internal/daemon/agn.go @@ -32,6 +32,7 @@ func newAgnDaemon(ctx context.Context, cfg config.Config, version string) (*Daem cfg.LLMAPIToken, setup.agent.GetModel(), summarization, + cfg.MCPHost, cfg.MCPServers, ) if err != nil { @@ -39,7 +40,7 @@ func newAgnDaemon(ctx context.Context, cfg config.Config, version string) (*Daem return nil, err } - if err := waitForMCPServers(ctx, cfg.MCPServers, mcpReadyTimeout); err != nil { + if err := waitForMCPServers(ctx, cfg.MCPServers, cfg.MCPHost, mcpReadyTimeout); err != nil { _ = setup.gatewayConn.Close() return nil, err } diff --git a/internal/daemon/agnconfig.go b/internal/daemon/agnconfig.go index 8d2fbbf..84a2015 100644 --- a/internal/daemon/agnconfig.go +++ b/internal/daemon/agnconfig.go @@ -16,7 +16,7 @@ const agnConfigTemplate = `llm: model: %s ` -func writeAgnConfig(llmBaseURL, apiKey, model string, summarization *summarizationConfig, mcpServers []config.MCPServer) (string, string, error) { +func writeAgnConfig(llmBaseURL, apiKey, model string, summarization *summarizationConfig, mcpHost string, mcpServers []config.MCPServer) (string, string, error) { home, err := os.UserHomeDir() if err != nil { return "", "", fmt.Errorf("resolve home directory: %w", err) @@ -26,14 +26,14 @@ func writeAgnConfig(llmBaseURL, apiKey, model string, summarization *summarizati return "", "", fmt.Errorf("create agn config dir: %w", err) } configPath := filepath.Join(agnDir, "config.yaml") - payload := agnConfig(llmBaseURL, apiKey, model, summarization, mcpServers) + payload := agnConfig(llmBaseURL, apiKey, model, summarization, mcpHost, mcpServers) if err := os.WriteFile(configPath, []byte(payload), 0o600); err != nil { return "", "", fmt.Errorf("write agn config: %w", err) } return agnDir, configPath, nil } -func agnConfig(llmBaseURL, apiKey, model string, summarization *summarizationConfig, mcpServers []config.MCPServer) string { +func agnConfig(llmBaseURL, apiKey, model string, summarization *summarizationConfig, mcpHost string, mcpServers []config.MCPServer) string { payload := fmt.Sprintf(agnConfigTemplate, llmBaseURL, apiKey, model) if summarization == nil && len(mcpServers) == 0 { return payload @@ -46,7 +46,7 @@ func agnConfig(llmBaseURL, apiKey, model string, summarization *summarizationCon if len(mcpServers) > 0 { builder.WriteString("mcp:\n servers:\n") for _, server := range mcpServers { - url := fmt.Sprintf("http://localhost:%d/mcp", server.Port) + url := mcpServerURL(mcpHost, server.Port) fmt.Fprintf(&builder, " %s:\n url: %s\n", server.Name, url) } } diff --git a/internal/daemon/agnconfig_test.go b/internal/daemon/agnconfig_test.go index a58ccd8..5843d32 100644 --- a/internal/daemon/agnconfig_test.go +++ b/internal/daemon/agnconfig_test.go @@ -16,7 +16,7 @@ func TestWriteAgnConfig(t *testing.T) { baseURL := "https://example.com" apiKey := "test-api-key" model := "test-model-id" - agnDir, configPath, err := writeAgnConfig(baseURL, apiKey, model, nil, nil) + agnDir, configPath, err := writeAgnConfig(baseURL, apiKey, model, nil, "127.0.0.1", nil) if err != nil { t.Fatalf("expected config to be written, got %v", err) } @@ -56,7 +56,7 @@ func TestAgnConfigNoSummarizationInJSON(t *testing.T) { t.Fatalf("expected nil summarization, got %#v", summarization) } - content := agnConfig(baseURL, apiKey, model, summarization, nil) + content := agnConfig(baseURL, apiKey, model, summarization, "127.0.0.1", nil) expected := fmt.Sprintf(agnConfigTemplate, baseURL, apiKey, model) if content != expected { t.Fatalf("expected config %q, got %q", expected, content) @@ -74,7 +74,7 @@ func TestWriteAgnConfigWithMCPServers(t *testing.T) { {Name: "memory", Port: 8100}, {Name: "filesystem", Port: 8200}, } - agnDir, configPath, err := writeAgnConfig(baseURL, apiKey, model, nil, mcpServers) + agnDir, configPath, err := writeAgnConfig(baseURL, apiKey, model, nil, "127.0.0.1", mcpServers) if err != nil { t.Fatalf("expected config to be written, got %v", err) } @@ -96,8 +96,8 @@ func TestWriteAgnConfigWithMCPServers(t *testing.T) { expected := fmt.Sprintf(agnConfigTemplate, baseURL, apiKey, model) + "mcp:\n servers:\n" + - " memory:\n url: http://localhost:8100/mcp\n" + - " filesystem:\n url: http://localhost:8200/mcp\n" + " memory:\n url: http://127.0.0.1:8100/mcp\n" + + " filesystem:\n url: http://127.0.0.1:8200/mcp\n" if string(content) != expected { t.Fatalf("expected config %q, got %q", expected, string(content)) } @@ -116,7 +116,7 @@ func TestWriteAgnConfigWithSummarizationThresholds(t *testing.T) { KeepTokens: &keepTokens, MaxTokens: &maxTokens, } - _, configPath, err := writeAgnConfig(baseURL, apiKey, model, summarization, nil) + _, configPath, err := writeAgnConfig(baseURL, apiKey, model, summarization, "127.0.0.1", nil) if err != nil { t.Fatalf("expected config to be written, got %v", err) } @@ -155,7 +155,7 @@ func TestWriteAgnConfigWithSummarizationLLMAPIKey(t *testing.T) { Model: "gpt-4.1-mini", }, } - agnDir, configPath, err := writeAgnConfig(baseURL, apiKey, model, summarization, nil) + agnDir, configPath, err := writeAgnConfig(baseURL, apiKey, model, summarization, "127.0.0.1", nil) if err != nil { t.Fatalf("expected config to be written, got %v", err) } @@ -209,7 +209,7 @@ func TestWriteAgnConfigWithSummarizationLLMAPIKeyEnv(t *testing.T) { Model: "gpt-4.1-mini", }, } - _, configPath, err := writeAgnConfig(baseURL, apiKey, model, summarization, nil) + _, configPath, err := writeAgnConfig(baseURL, apiKey, model, summarization, "127.0.0.1", nil) if err != nil { t.Fatalf("expected config to be written, got %v", err) } diff --git a/internal/daemon/claude.go b/internal/daemon/claude.go index c957131..0a8a53c 100644 --- a/internal/daemon/claude.go +++ b/internal/daemon/claude.go @@ -21,12 +21,12 @@ func newClaudeDaemon(ctx context.Context, cfg config.Config, version string) (*D return nil, err } - if err := writeClaudeSettings(claudeBaseURL(cfg.LLMBaseURL), cfg.LLMAPIToken, cfg.MCPServers); err != nil { + if err := writeClaudeSettings(claudeBaseURL(cfg.LLMBaseURL), cfg.LLMAPIToken, cfg.MCPHost, cfg.MCPServers); err != nil { _ = setup.gatewayConn.Close() return nil, err } - if err := waitForMCPServers(ctx, cfg.MCPServers, mcpReadyTimeout); err != nil { + if err := waitForMCPServers(ctx, cfg.MCPServers, cfg.MCPHost, mcpReadyTimeout); err != nil { _ = setup.gatewayConn.Close() return nil, err } @@ -47,6 +47,7 @@ func newClaudeDaemon(ctx context.Context, cfg config.Config, version string) (*D Env: []string{ "LD_LIBRARY_PATH=/agyn-bin/lib", "OTEL_EXPORTER_OTLP_ENDPOINT=" + otlpEndpoint, + "IS_SANDBOX=1", }, } if model := strings.TrimSpace(setup.agent.GetModel()); model != "" { diff --git a/internal/daemon/claudeconfig.go b/internal/daemon/claudeconfig.go index 4188838..0b128a1 100644 --- a/internal/daemon/claudeconfig.go +++ b/internal/daemon/claudeconfig.go @@ -27,7 +27,7 @@ type claudeMCPServer struct { URL string `json:"url"` } -func writeClaudeSettings(llmBaseURL, apiKey string, mcpServers []config.MCPServer) error { +func writeClaudeSettings(llmBaseURL, apiKey, mcpHost string, mcpServers []config.MCPServer) error { home, err := os.UserHomeDir() if err != nil { return fmt.Errorf("resolve home directory: %w", err) @@ -68,7 +68,7 @@ func writeClaudeSettings(llmBaseURL, apiKey string, mcpServers []config.MCPServe for _, server := range mcpServers { settings.MCPServers[server.Name] = claudeMCPServer{ Type: "http", - URL: fmt.Sprintf("http://localhost:%d/mcp", server.Port), + URL: mcpServerURL(mcpHost, server.Port), } } } diff --git a/internal/daemon/claudeconfig_test.go b/internal/daemon/claudeconfig_test.go index 7be088d..f1e1924 100644 --- a/internal/daemon/claudeconfig_test.go +++ b/internal/daemon/claudeconfig_test.go @@ -16,7 +16,7 @@ func TestWriteClaudeSettings(t *testing.T) { baseURL := "https://example.com" apiKey := "test-api-key" - if err := writeClaudeSettings(baseURL, apiKey, nil); err != nil { + if err := writeClaudeSettings(baseURL, apiKey, "127.0.0.1", nil); err != nil { t.Fatalf("expected settings to be written, got %v", err) } @@ -73,7 +73,7 @@ func TestWriteClaudeSettingsWithMCPServers(t *testing.T) { {Name: "memory", Port: 8100}, {Name: "cache", Port: 8200}, } - if err := writeClaudeSettings(baseURL, apiKey, mcpServers); err != nil { + if err := writeClaudeSettings(baseURL, apiKey, "127.0.0.1", mcpServers); err != nil { t.Fatalf("expected settings to be written, got %v", err) } @@ -115,8 +115,8 @@ func TestWriteClaudeSettingsWithMCPServers(t *testing.T) { "DISABLE_AUTOUPDATER": "1", }, MCPServers: map[string]claudeMCPServer{ - "memory": {Type: "http", URL: "http://localhost:8100/mcp"}, - "cache": {Type: "http", URL: "http://localhost:8200/mcp"}, + "memory": {Type: "http", URL: "http://127.0.0.1:8100/mcp"}, + "cache": {Type: "http", URL: "http://127.0.0.1:8200/mcp"}, }, } if !reflect.DeepEqual(got, expected) { diff --git a/internal/daemon/codexconfig.go b/internal/daemon/codexconfig.go index fdb5f12..e8be3e5 100644 --- a/internal/daemon/codexconfig.go +++ b/internal/daemon/codexconfig.go @@ -5,6 +5,7 @@ import ( "os" "path/filepath" "strings" + "time" "github.com/agynio/agynd-cli/internal/config" ) @@ -20,7 +21,7 @@ env_key = "OPENAI_API_KEY" wire_api = "responses" ` -func writeCodexConfig(llmBaseURL string, mcpServers []config.MCPServer) (string, error) { +func writeCodexConfig(llmBaseURL, mcpHost string, mcpServers []config.MCPServer) (string, error) { home, err := os.UserHomeDir() if err != nil { return "", fmt.Errorf("resolve home directory: %w", err) @@ -30,14 +31,14 @@ func writeCodexConfig(llmBaseURL string, mcpServers []config.MCPServer) (string, return "", fmt.Errorf("create codex home dir: %w", err) } configPath := filepath.Join(codexHome, "config.toml") - payload := codexConfig(llmBaseURL, mcpServers) + payload := codexConfig(llmBaseURL, mcpHost, mcpServers) if err := os.WriteFile(configPath, []byte(payload), 0o600); err != nil { return "", fmt.Errorf("write codex config: %w", err) } return codexHome, nil } -func codexConfig(llmBaseURL string, mcpServers []config.MCPServer) string { +func codexConfig(llmBaseURL, mcpHost string, mcpServers []config.MCPServer) string { payload := fmt.Sprintf(codexConfigTemplate, llmBaseURL) if len(mcpServers) == 0 { return payload @@ -45,8 +46,9 @@ func codexConfig(llmBaseURL string, mcpServers []config.MCPServer) string { var builder strings.Builder builder.WriteString(payload) for _, server := range mcpServers { - url := fmt.Sprintf("http://localhost:%d/mcp", server.Port) - fmt.Fprintf(&builder, "\n[mcp_servers.%s]\nurl = %q\nrequired = true\nstartup_timeout_sec = 120\n", server.Name, url) + url := mcpServerURL(mcpHost, server.Port) + startupTimeout := int(mcpReadyTimeout / time.Second) + fmt.Fprintf(&builder, "\n[mcp_servers.%s]\nurl = %q\nrequired = true\nstartup_timeout_sec = %d\n", server.Name, url, startupTimeout) } return builder.String() } diff --git a/internal/daemon/codexconfig_test.go b/internal/daemon/codexconfig_test.go index 42ca9a1..64ff8dd 100644 --- a/internal/daemon/codexconfig_test.go +++ b/internal/daemon/codexconfig_test.go @@ -14,7 +14,7 @@ func TestWriteCodexConfig(t *testing.T) { t.Setenv("HOME", tmpHome) baseURL := "https://example.com" - codexHome, err := writeCodexConfig(baseURL, nil) + codexHome, err := writeCodexConfig(baseURL, "127.0.0.1", nil) if err != nil { t.Fatalf("expected config to be written, got %v", err) } @@ -54,7 +54,7 @@ func TestWriteCodexConfigWithMCPServers(t *testing.T) { {Name: "memory", Port: 8100}, {Name: "cache", Port: 8200}, } - codexHome, err := writeCodexConfig(baseURL, mcpServers) + codexHome, err := writeCodexConfig(baseURL, "127.0.0.1", mcpServers) if err != nil { t.Fatalf("expected config to be written, got %v", err) } @@ -81,13 +81,13 @@ env_key = "OPENAI_API_KEY" wire_api = "responses" `, baseURL) + "\n[mcp_servers.memory]\n" + - "url = \"http://localhost:8100/mcp\"\n" + + "url = \"http://127.0.0.1:8100/mcp\"\n" + "required = true\n" + - "startup_timeout_sec = 120\n" + + "startup_timeout_sec = 180\n" + "\n[mcp_servers.cache]\n" + - "url = \"http://localhost:8200/mcp\"\n" + + "url = \"http://127.0.0.1:8200/mcp\"\n" + "required = true\n" + - "startup_timeout_sec = 120\n" + "startup_timeout_sec = 180\n" if string(content) != expected { t.Fatalf("expected config %q, got %q", expected, string(content)) } diff --git a/internal/daemon/daemon.go b/internal/daemon/daemon.go index d82e7d9..e9ac3f0 100644 --- a/internal/daemon/daemon.go +++ b/internal/daemon/daemon.go @@ -29,7 +29,10 @@ const ( turnCompletionTimeout = 5 * time.Minute messagePublishTimeout = 15 * time.Second messageAckTimeout = 15 * time.Second - mcpReadyTimeout = 120 * time.Second + mcpReadyTimeout = 3 * time.Minute + mcpReadyDialTimeout = 2 * time.Second + mcpRetryInitialDelay = 500 * time.Millisecond + mcpRetryMaxDelay = 5 * time.Second ) const ( @@ -189,7 +192,7 @@ func newCodexDaemon(ctx context.Context, cfg config.Config, version string) (*Da tracker := codexbridge.NewTurnTracker() bridge := codexbridge.New(tracker) threadsMapping := codexbridge.NewThreadMapping() - codexHome, err := writeCodexConfig(cfg.LLMBaseURL, cfg.MCPServers) + codexHome, err := writeCodexConfig(cfg.LLMBaseURL, cfg.MCPHost, cfg.MCPServers) if err != nil { _ = setup.gatewayConn.Close() return nil, err @@ -381,7 +384,7 @@ func (d *Daemon) ensureCodexThread(ctx context.Context, platformThreadID string) } return record.CodexThreadID, nil } - if err := waitForMCPServers(ctx, d.cfg.MCPServers, mcpReadyTimeout); err != nil { + if err := waitForMCPServers(ctx, d.cfg.MCPServers, d.cfg.MCPHost, mcpReadyTimeout); err != nil { return "", err } record, ok, err := d.mappingStore.Load(platformThreadID) @@ -417,33 +420,46 @@ func (d *Daemon) ensureCodexThread(ctx context.Context, platformThreadID string) return codexThreadID, nil } -func waitForMCPServers(ctx context.Context, servers []config.MCPServer, timeout time.Duration) error { +func waitForMCPServers(ctx context.Context, servers []config.MCPServer, host string, timeout time.Duration) error { if len(servers) == 0 { return nil } + for _, server := range servers { + if err := waitForMCPServer(ctx, server, host, timeout); err != nil { + return err + } + } + return nil +} + +func waitForMCPServer(ctx context.Context, server config.MCPServer, host string, timeout time.Duration) error { + addr := mcpServerAddress(host, server.Port) deadline := time.NewTimer(timeout) defer deadline.Stop() - for _, server := range servers { - addr := fmt.Sprintf("localhost:%d", server.Port) - attempt := 0 - for { - attempt++ - conn, err := net.DialTimeout("tcp", addr, 1*time.Second) - if err == nil { - _ = conn.Close() - break - } - log.Printf("waiting for MCP server %s at %s (attempt %d): %v", server.Name, addr, attempt, err) - select { - case <-ctx.Done(): - return ctx.Err() - case <-deadline.C: - return fmt.Errorf("MCP server %s at %s not ready after %s", server.Name, addr, timeout) - case <-time.After(2 * time.Second): + delay := mcpRetryInitialDelay + attempt := 0 + for { + attempt++ + conn, err := net.DialTimeout("tcp", addr, mcpReadyDialTimeout) + if err == nil { + _ = conn.Close() + return nil + } + log.Printf("waiting for MCP server %s at %s (attempt %d): %v", server.Name, addr, attempt, err) + select { + case <-ctx.Done(): + return ctx.Err() + case <-deadline.C: + return fmt.Errorf("MCP server %s at %s not ready after %s", server.Name, addr, timeout) + case <-time.After(delay): + } + if delay < mcpRetryMaxDelay { + delay *= 2 + if delay > mcpRetryMaxDelay { + delay = mcpRetryMaxDelay } } } - return nil } type codexThreadDefaults struct { diff --git a/internal/daemon/daemon_mcp_test.go b/internal/daemon/daemon_mcp_test.go index c17c514..3358ff5 100644 --- a/internal/daemon/daemon_mcp_test.go +++ b/internal/daemon/daemon_mcp_test.go @@ -24,7 +24,7 @@ func TestWaitForMCPServersReady(t *testing.T) { {Name: "alpha", Port: portA}, {Name: "beta", Port: portB}, } - if err := waitForMCPServers(ctx, servers, 500*time.Millisecond); err != nil { + if err := waitForMCPServers(ctx, servers, "127.0.0.1", 500*time.Millisecond); err != nil { t.Fatalf("expected MCP servers to be ready, got %v", err) } } @@ -33,7 +33,7 @@ func TestWaitForMCPServersEmpty(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond) defer cancel() - if err := waitForMCPServers(ctx, nil, 10*time.Millisecond); err != nil { + if err := waitForMCPServers(ctx, nil, "127.0.0.1", 10*time.Millisecond); err != nil { t.Fatalf("expected no error for empty server list, got %v", err) } } @@ -41,7 +41,7 @@ func TestWaitForMCPServersEmpty(t *testing.T) { func TestWaitForMCPServersTimeout(t *testing.T) { port := unusedTCPPort(t) - err := waitForMCPServers(context.Background(), []config.MCPServer{{Name: "missing", Port: port}}, 50*time.Millisecond) + err := waitForMCPServers(context.Background(), []config.MCPServer{{Name: "missing", Port: port}}, "127.0.0.1", 50*time.Millisecond) if err == nil { t.Fatal("expected timeout error, got nil") } @@ -57,7 +57,7 @@ func TestWaitForMCPServersContextCanceled(t *testing.T) { timer := time.AfterFunc(20*time.Millisecond, cancel) defer timer.Stop() - err := waitForMCPServers(ctx, []config.MCPServer{{Name: "missing", Port: port}}, 5*time.Second) + err := waitForMCPServers(ctx, []config.MCPServer{{Name: "missing", Port: port}}, "127.0.0.1", 5*time.Second) if !errors.Is(err, context.Canceled) { t.Fatalf("expected context cancellation, got %v", err) } diff --git a/internal/daemon/mcp.go b/internal/daemon/mcp.go new file mode 100644 index 0000000..8e337c0 --- /dev/null +++ b/internal/daemon/mcp.go @@ -0,0 +1,20 @@ +package daemon + +import ( + "net" + "net/url" + "strconv" +) + +func mcpServerURL(host string, port int) string { + endpoint := url.URL{ + Scheme: "http", + Host: net.JoinHostPort(host, strconv.Itoa(port)), + Path: "/mcp", + } + return endpoint.String() +} + +func mcpServerAddress(host string, port int) string { + return net.JoinHostPort(host, strconv.Itoa(port)) +}