Skip to content

Commit 9d81873

Browse files
committed
fix: stdio server
1 parent 8585d7f commit 9d81873

File tree

7 files changed

+59
-162
lines changed

7 files changed

+59
-162
lines changed

.cursor/settings.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
{
2-
"mcp.executablePath": "/Users/harvey/Work/dev/FreePeak/Opensource/cortex/bin/echo-stdio-server",
2+
"mcp.executablePath": "/Users/harvey/Work/dev/FreePeak/Opensource/cortex/bin/stdio-server",
33
"mcp.transport": "stdio",
4-
"mcp.logging": true
4+
"mcp.logging": true,
5+
"mcp.logLevel": "debug"
56
}

bin/multi-protocol-server

-160 Bytes
Binary file not shown.

bin/sse-server

0 Bytes
Binary file not shown.

bin/stdio-server

432 Bytes
Binary file not shown.

examples/stdio-server/main.go

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,9 @@ func getTimestamp() string {
1818
}
1919

2020
func main() {
21-
// Create a logger
22-
logger := log.New(os.Stdout, "[cortex-stdio] ", log.LstdFlags)
21+
// Create a logger that writes to stderr instead of stdout
22+
// This is critical for STDIO servers as stdout must only contain JSON-RPC messages
23+
logger := log.New(os.Stderr, "[cortex-stdio] ", log.LstdFlags)
2324

2425
// Create the server with name and version
2526
mcpServer := server.NewMCPServer("Cortex Stdio Server", "1.0.0", logger)
@@ -57,15 +58,12 @@ func main() {
5758
logger.Fatalf("Error adding weather tool: %v", err)
5859
}
5960

60-
// Print server ready message
61-
fmt.Println("Server ready. You can now send JSON-RPC requests via stdin.")
62-
fmt.Println("The following tools are available:")
63-
fmt.Println("- echo / cortex_echo")
64-
fmt.Println("- weather / cortex_weather")
65-
fmt.Println("Example call:")
66-
fmt.Println(`{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"echo","parameters":{"message":"Hello, World!"}}}`)
61+
// Write server status to stderr instead of stdout to maintain clean JSON protocol
62+
fmt.Fprintf(os.Stderr, "Server ready. The following tools are available:\n")
63+
fmt.Fprintf(os.Stderr, "- echo\n")
64+
fmt.Fprintf(os.Stderr, "- weather\n")
6765

68-
// Start the server
66+
// Start the STDIO server
6967
if err := mcpServer.ServeStdio(); err != nil {
7068
fmt.Fprintf(os.Stderr, "Error serving stdio: %v\n", err)
7169
os.Exit(1)
@@ -74,8 +72,8 @@ func main() {
7472

7573
// Echo tool handler
7674
func handleEcho(ctx context.Context, request server.ToolCallRequest) (interface{}, error) {
77-
// Log request details
78-
log.Printf("Handling echo request with name: %s", request.Name)
75+
// Log request details to stderr via the logger
76+
log.Printf("Handling echo tool call with name: %s", request.Name)
7977

8078
// Extract the message parameter
8179
message, ok := request.Parameters["message"].(string)
@@ -100,8 +98,8 @@ func handleEcho(ctx context.Context, request server.ToolCallRequest) (interface{
10098

10199
// Weather tool handler
102100
func handleWeather(ctx context.Context, request server.ToolCallRequest) (interface{}, error) {
103-
// Log request details
104-
log.Printf("Handling weather request with name: %s", request.Name)
101+
// Log request details to stderr via the logger
102+
log.Printf("Handling weather tool call with name: %s", request.Name)
105103

106104
// Extract the location parameter
107105
location, ok := request.Parameters["location"].(string)

internal/interfaces/stdio/server.go

Lines changed: 37 additions & 145 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import (
88
"fmt"
99
"io"
1010
"log"
11-
"math/rand"
1211
"os"
1312
"os/signal"
1413
"strings"
@@ -18,6 +17,7 @@ import (
1817
"github.com/FreePeak/cortex/internal/domain"
1918
"github.com/FreePeak/cortex/internal/infrastructure/logging"
2019
"github.com/FreePeak/cortex/internal/interfaces/rest"
20+
"github.com/google/uuid"
2121
)
2222

2323
// Constants for JSON-RPC
@@ -70,11 +70,12 @@ func WithStdioContextFunc(fn StdioContextFunc) StdioOption {
7070
// It will create a custom logger that wraps the standard log.Logger
7171
func WithErrorLogger(stdLogger *log.Logger) StdioOption {
7272
return func(s *StdioServer) {
73-
// Create a development logger that outputs to stderr
73+
// Create a development logger that always outputs to stderr
74+
// In STDIO mode, stdout is reserved for JSON-RPC messages only
7475
logger, err := logging.New(logging.Config{
7576
Level: logging.InfoLevel,
7677
Development: true,
77-
OutputPaths: []string{"stderr"},
78+
OutputPaths: []string{"stderr"}, // Force stderr for all logging
7879
})
7980
if err != nil {
8081
// If we can't create the logger, use the default one
@@ -87,11 +88,11 @@ func WithErrorLogger(stdLogger *log.Logger) StdioOption {
8788
// NewStdioServer creates a new stdio server wrapper around an MCPServer.
8889
// It initializes the server with a default logger that logs to stderr.
8990
func NewStdioServer(server *rest.MCPServer, opts ...StdioOption) *StdioServer {
90-
// Create default logger
91+
// Create default logger - always use stderr for STDIO servers
9192
defaultLogger, err := logging.New(logging.Config{
9293
Level: logging.InfoLevel,
9394
Development: true,
94-
OutputPaths: []string{"stderr"},
95+
OutputPaths: []string{"stderr"}, // Force stderr for all logging output
9596
InitialFields: logging.Fields{
9697
"component": "stdio-server",
9798
},
@@ -453,160 +454,51 @@ func (p *MessageProcessor) handleToolsCall(ctx context.Context, params interface
453454
}
454455
}
455456

456-
// Get available tools from the service
457-
tools, err := p.server.GetService().ListTools(ctx)
458-
if err != nil {
459-
return nil, &domain.JSONRPCError{
460-
Code: InternalErrorCode,
461-
Message: fmt.Sprintf("Internal error: %v", err),
462-
}
457+
// Create a client session for the tool handler
458+
clientSession := &domain.ClientSession{
459+
ID: generateSessionID(),
460+
Connected: true,
463461
}
464462

465-
// Find the requested tool
466-
var foundTool *domain.Tool
467-
var toolFound bool
468-
469-
for _, tool := range tools {
470-
if tool.Name == toolName {
471-
foundTool = tool
472-
toolFound = true
473-
break
474-
}
475-
}
476-
477-
if !toolFound {
478-
return nil, &domain.JSONRPCError{
479-
Code: MethodNotFoundCode,
480-
Message: fmt.Sprintf("Tool not found: %s", toolName),
481-
}
482-
}
463+
// Access the service to get the tool handler
464+
service := p.server.GetService()
483465

484-
// Validate required parameters
485-
missingParams := []string{}
486-
for _, param := range foundTool.Parameters {
487-
if param.Required {
488-
paramValue, exists := toolParams[param.Name]
489-
if !exists || paramValue == nil {
490-
missingParams = append(missingParams, param.Name)
466+
// Try to get a registered handler for this tool
467+
handler := service.GetToolHandler(toolName)
468+
if handler != nil {
469+
// We have a registered handler, use it
470+
p.logger.Info("Using registered handler for tool", logging.Fields{"tool": toolName})
471+
result, err := handler(ctx, toolParams, clientSession)
472+
if err != nil {
473+
p.logger.Error("Error executing tool handler", logging.Fields{"tool": toolName, "error": err})
474+
return nil, &domain.JSONRPCError{
475+
Code: InternalErrorCode,
476+
Message: fmt.Sprintf("Error executing tool: %v", err),
491477
}
492478
}
493-
}
494-
495-
if len(missingParams) > 0 {
496-
return nil, &domain.JSONRPCError{
497-
Code: InvalidParamsCode,
498-
Message: fmt.Sprintf("Missing required parameters: %s", strings.Join(missingParams, ", ")),
499-
}
500-
}
501-
502-
// Handle different tool types using a strategy pattern
503-
var toolResult interface{}
504-
var toolErr error
505-
506-
// Use exact tool name matching instead of prefix matching
507-
switch toolName {
508-
case "mcp_golang_mcp_server_stdio_echo", "mcp_golang_mcp_server_stdio_cortex_echo":
509-
toolResult, toolErr = handleEchoTool(toolParams)
510-
case "mcp_golang_mcp_server_stdio_weather", "mcp_golang_mcp_server_stdio_cortex_weather":
511-
toolResult, toolErr = handleWeatherTool(toolParams)
512-
default:
513-
return nil, &domain.JSONRPCError{
514-
Code: InternalErrorCode,
515-
Message: fmt.Sprintf("Tool '%s' is registered but has no implementation", toolName),
516-
}
517-
}
518479

519-
if toolErr != nil {
520-
return nil, &domain.JSONRPCError{
521-
Code: InternalErrorCode,
522-
Message: toolErr.Error(),
523-
}
480+
p.logger.Info("Tool executed successfully", logging.Fields{"tool": toolName})
481+
return result, nil
524482
}
525483

526-
return toolResult, nil
527-
}
484+
// No handler found - log available handlers and return error
485+
availableHandlers := service.GetAllToolHandlerNames()
486+
p.logger.Warn("No registered handler found for tool", logging.Fields{
487+
"tool": toolName,
488+
"availableHandlers": fmt.Sprintf("%+v", availableHandlers),
489+
})
528490

529-
// Handle echo tool types
530-
func handleEchoTool(params map[string]interface{}) (interface{}, error) {
531-
var message string
532-
533-
// Extract message parameter
534-
messageVal, exists := params["message"]
535-
if !exists || messageVal == nil {
536-
return nil, fmt.Errorf("missing 'message' parameter")
537-
}
538-
539-
// Convert to string based on type
540-
switch v := messageVal.(type) {
541-
case string:
542-
message = v
543-
case float64, int, int64, float32:
544-
message = fmt.Sprintf("%v", v)
545-
default:
546-
// Try JSON conversion for complex types
547-
jsonBytes, err := json.Marshal(v)
548-
if err != nil {
549-
message = fmt.Sprintf("%v", v)
550-
} else {
551-
message = string(jsonBytes)
552-
}
491+
return nil, &domain.JSONRPCError{
492+
Code: InternalErrorCode,
493+
Message: fmt.Sprintf("Tool '%s' is registered but has no implementation", toolName),
553494
}
554-
555-
// Return formatted result using the MCP content format
556-
return map[string]interface{}{
557-
"content": []map[string]interface{}{
558-
{
559-
"type": "text",
560-
"text": message,
561-
},
562-
},
563-
}, nil
564495
}
565496

566-
// Handle weather tool types
567-
func handleWeatherTool(params map[string]interface{}) (interface{}, error) {
568-
// Extract location parameter
569-
locationVal, exists := params["location"]
570-
if !exists || locationVal == nil {
571-
return nil, fmt.Errorf("missing 'location' parameter")
572-
}
573-
574-
location, ok := locationVal.(string)
575-
if !ok {
576-
return nil, fmt.Errorf("location must be a string")
577-
}
578-
579-
// Generate random weather data for testing
580-
rand.Seed(time.Now().UnixNano())
581-
conditions := []string{"Sunny", "Partly Cloudy", "Cloudy", "Rainy", "Thunderstorms", "Snowy", "Foggy", "Windy"}
582-
tempF := rand.Intn(50) + 30 // Random temperature between 30°F and 80°F
583-
tempC := (tempF - 32) * 5 / 9
584-
humidity := rand.Intn(60) + 30 // Random humidity between 30% and 90%
585-
windSpeed := rand.Intn(20) + 5 // Random wind speed between 5-25mph
586-
587-
// Select a random condition
588-
condition := conditions[rand.Intn(len(conditions))]
589-
590-
// Format today's date
591-
today := time.Now().Format("Monday, January 2, 2006")
592-
593-
// Format the weather response
594-
weatherInfo := fmt.Sprintf("Weather for %s on %s:\nCondition: %s\nTemperature: %d°F (%d°C)\nHumidity: %d%%\nWind Speed: %d mph",
595-
location, today, condition, tempF, tempC, humidity, windSpeed)
596-
597-
// Return the weather response in the format expected by the MCP protocol
598-
return map[string]interface{}{
599-
"content": []map[string]interface{}{
600-
{
601-
"type": "text",
602-
"text": weatherInfo,
603-
},
604-
},
605-
}, nil
497+
// generateSessionID creates a unique session ID
498+
func generateSessionID() string {
499+
return uuid.New().String()
606500
}
607501

608-
// Helper functions for error handling and response creation
609-
610502
// isTerminalError determines if an error should cause the server to shut down
611503
func isTerminalError(err error) bool {
612504
if err == nil {

pkg/server/server.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -222,14 +222,20 @@ func (s *MCPServer) UnregisterProvider(ctx context.Context, providerID string) e
222222

223223
// ServeStdio serves the MCP server over standard I/O.
224224
func (s *MCPServer) ServeStdio() error {
225+
// In STDIO mode, we must write all logs to stderr, as stdout is reserved for JSON-RPC messages
225226
s.logger.Printf("Starting MCP server over stdio: %s v%s", s.name, s.version)
226227

227228
// Create stdio options
228229
var stdioOpts []stdio.StdioOption
229230

230-
// Add the default error logger
231+
// Always use stderr for logging in STDIO mode
231232
stdioOpts = append(stdioOpts, stdio.WithErrorLogger(s.logger))
232233

234+
// Log registered tools for debugging
235+
service := s.builder.BuildService()
236+
toolHandlers := service.GetAllToolHandlerNames()
237+
s.logger.Printf("Available tools in the server: %v", toolHandlers)
238+
233239
// Start the stdio server with our custom handler
234240
return s.builder.ServeStdio(stdioOpts...)
235241
}

0 commit comments

Comments
 (0)