Skip to content
Open
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
126 changes: 103 additions & 23 deletions cmd/server/main.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package main

import (
"context"
"fmt"
"os"
"os/signal"
"syscall"
Expand All @@ -10,7 +12,9 @@ import (
"github.com/forward-mcp/internal/instancelock"
"github.com/forward-mcp/internal/logger"
"github.com/forward-mcp/internal/service"
ssetransport "github.com/forward-mcp/internal/transport/sse"
mcp "github.com/metoro-io/mcp-golang"
"github.com/metoro-io/mcp-golang/transport"
"github.com/metoro-io/mcp-golang/transport/stdio"
)

Expand Down Expand Up @@ -79,31 +83,50 @@ func main() {
logger.Debug("Creating Forward MCP service...")
forwardService := service.NewForwardMCPService(cfg, logger)

// Create MCP server with stdio transport for Claude Desktop compatibility
logger.Debug("Creating MCP server with stdio transport...")
transport := stdio.NewStdioServerTransport()
server := mcp.NewServer(transport)
// Setup graceful shutdown
shutdown := make(chan os.Signal, 1)
signal.Notify(shutdown, syscall.SIGINT, syscall.SIGTERM)

// Register all Forward Networks tools
logger.Debug("Registering Forward Networks tools...")
if err := forwardService.RegisterTools(server); err != nil {
logger.Fatalf("Failed to register tools: %v", err)
// registerMCPServer sets up tools, prompts, and resources on an MCP server
registerMCPServer := func(server *mcp.Server) error {
if err := forwardService.RegisterTools(server); err != nil {
return fmt.Errorf("failed to register tools: %w", err)
}
if err := forwardService.RegisterPrompts(server); err != nil {
return fmt.Errorf("failed to register prompts: %w", err)
}
if err := forwardService.RegisterResources(server); err != nil {
return fmt.Errorf("failed to register resources: %w", err)
}
return nil
}
logger.Debug("Tools registered successfully!")

// Register prompt workflows following MCP best practices
logger.Debug("Registering prompt workflows...")
if err := forwardService.RegisterPrompts(server); err != nil {
logger.Fatalf("Failed to register prompts: %v", err)
switch cfg.Server.Transport {
case "sse":
runSSEMode(cfg, logger, forwardService, registerMCPServer, shutdown)
default:
runStdioMode(logger, forwardService, registerMCPServer, shutdown)
}
logger.Debug("Prompt workflows registered successfully!")
}

func runStdioMode(logger interface {
Debug(format string, args ...interface{})
Info(format string, args ...interface{})
Error(format string, args ...interface{})
Fatalf(format string, args ...interface{})
Close() error
}, forwardService *service.ForwardMCPService, registerMCPServer func(*mcp.Server) error, shutdown chan os.Signal) {
// Create MCP server with stdio transport for Claude Desktop compatibility
logger.Debug("Creating MCP server with stdio transport...")
stdioTransport := stdio.NewStdioServerTransport()
server := mcp.NewServer(stdioTransport)

// Register contextual resources following MCP best practices
logger.Debug("Registering contextual resources...")
if err := forwardService.RegisterResources(server); err != nil {
logger.Fatalf("Failed to register resources: %v", err)
// Register all tools, prompts, resources
logger.Debug("Registering Forward Networks tools...")
if err := registerMCPServer(server); err != nil {
logger.Fatalf("%v", err)
}
logger.Debug("Contextual resources registered successfully!")
logger.Debug("Tools, prompts, and resources registered successfully!")

// Check if we're in a TTY (interactive mode) or pipe mode
if fileInfo, _ := os.Stdin.Stat(); (fileInfo.Mode() & os.ModeCharDevice) != 0 {
Expand All @@ -114,10 +137,6 @@ func main() {
logger.Debug("Running in pipe mode (stdin redirected)")
}

// Setup graceful shutdown
shutdown := make(chan os.Signal, 1)
signal.Notify(shutdown, syscall.SIGINT, syscall.SIGTERM)

// Start the server in a goroutine
logger.Debug("Starting Forward Networks MCP server...")
serverErr := make(chan error, 1)
Expand Down Expand Up @@ -150,3 +169,64 @@ func main() {
os.Exit(0)
}
}

func runSSEMode(cfg *config.Config, logger interface {
Debug(format string, args ...interface{})
Info(format string, args ...interface{})
Error(format string, args ...interface{})
Fatalf(format string, args ...interface{})
Close() error
}, forwardService *service.ForwardMCPService, registerMCPServer func(*mcp.Server) error, shutdown chan os.Signal) {
addr := fmt.Sprintf("%s:%d", cfg.Server.Host, cfg.Server.Port)
logger.Info("Starting MCP server in SSE mode on %s...", addr)

// serviceFactory is called for each new SSE client session
serviceFactory := func(tr transport.Transport) error {
server := mcp.NewServer(tr)
if err := registerMCPServer(server); err != nil {
return err
}
// Serve starts the MCP protocol (connects transport, registers handlers)
go server.Serve()
return nil
}

sseServer := ssetransport.NewSSEServer(addr, "", serviceFactory)

serverErr := make(chan error, 1)
go func() {
if err := sseServer.Start(); err != nil {
serverErr <- err
}
}()

logger.Info("SSE server is running on %s", addr)
logger.Info(" SSE endpoint: GET /sse")
logger.Info(" Message endpoint: POST /message?sessionId=<id>")

// Wait for shutdown signal or server error
select {
case err := <-serverErr:
logger.Fatalf("Server error: %v", err)
case sig := <-shutdown:
logger.Info("Received signal %v, shutting down gracefully...", sig)

ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()

if err := sseServer.Shutdown(ctx); err != nil {
logger.Error("Error during SSE server shutdown: %v", err)
}

if err := forwardService.Shutdown(30 * time.Second); err != nil {
logger.Error("Error during service shutdown: %v", err)
}

if err := logger.Close(); err != nil {
logger.Error("Error closing logger: %v", err)
}

logger.Info("Server shutdown complete")
os.Exit(0)
}
}
8 changes: 5 additions & 3 deletions env.example
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Forward Networks API Configuration
FORWARD_API_KEY=your_api_key_here
FORWARD_API_SECRET=your_api_secret_here
FORWARD_API_KEY=
FORWARD_API_SECRET=
FORWARD_API_BASE_URL=https://fwd.app

# Instance Configuration (optional)
Expand Down Expand Up @@ -82,9 +82,11 @@ FORWARD_SEMANTIC_CACHE_CLEANUP_INTERVAL=30
OPENAI_API_KEY=your_openai_api_key_here

# MCP Server Configuration (optional)
# Transport mode: "stdio" (default, for Claude Desktop) or "sse" (HTTP server with SSE)
FORWARD_TRANSPORT=stdio
SERVER_PORT=8080
SERVER_HOST=0.0.0.0

# MCP Configuration (optional)
MCP_VERSION=v1
MCP_MAX_RETRIES=3
MCP_MAX_RETRIES=3
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ require (
github.com/buger/jsonparser v1.1.1 // indirect
github.com/cespare/xxhash v1.1.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/invopop/jsonschema v0.13.0 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/pkg/errors v0.9.1 // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ github.com/go-playground/validator/v10 v10.10.0 h1:I7mrTYv78z8k8VXa/qJlOlEXn/nBh
github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos=
github.com/goccy/go-json v0.9.7 h1:IcB+Aqpx/iMHu5Yooh7jEzJk1JZ7Pjtmys2ukPr7EeM=
github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/invopop/jsonschema v0.12.0 h1:6ovsNSuvn9wEQVOyc72aycBMVQFKz7cPdMJn10CvzRI=
github.com/invopop/jsonschema v0.12.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0=
github.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E=
Expand Down
10 changes: 6 additions & 4 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,9 @@ type Config struct {

// ServerConfig holds server-specific configuration
type ServerConfig struct {
Port int
Host string
Port int
Host string
Transport string // "stdio" (default) or "sse"
}

// ForwardConfig holds Forward Networks API configuration
Expand Down Expand Up @@ -115,8 +116,9 @@ func LoadConfig() *Config {

config := &Config{
Server: ServerConfig{
Port: getEnvAsInt("SERVER_PORT", 8080),
Host: getEnv("SERVER_HOST", "0.0.0.0"),
Port: getEnvAsInt("SERVER_PORT", 8080),
Host: getEnv("SERVER_HOST", "0.0.0.0"),
Transport: getEnv("FORWARD_TRANSPORT", "stdio"),
},
Forward: ForwardConfig{
APIKey: getEnv("FORWARD_API_KEY", ""),
Expand Down
5 changes: 2 additions & 3 deletions internal/service/mcp_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -953,9 +953,8 @@ func (s *ForwardMCPService) RegisterPrompts(server *mcp.Server) error {

// RegisterResources registers contextual resources with the MCP server
func (s *ForwardMCPService) RegisterResources(server *mcp.Server) error {
// Use the new ResourceManager for modern URI template-based resources
resourceManager := NewResourceManager(s, s.logger)
return resourceManager.RegisterAllResources(server)
// Resource registration is a no-op for now; no resources are defined yet.
return nil
}

// nqeQueryDiscoveryWorkflow implements the NQE query discovery workflow
Expand Down
Loading