feat: add Streamable HTTP transport support
Add support for running the MCP server over HTTP with Server-Sent Events (SSE) using the MCP Streamable HTTP specification, alongside the existing STDIO transport. New features: - Transport abstraction with Transport interface - HTTP transport with session management - SSE support for server-initiated notifications - CORS security with configurable allowed origins - Optional TLS support - CLI flags for HTTP configuration (--transport, --http-address, etc.) - NixOS module options for HTTP transport The HTTP transport implements: - POST /mcp: JSON-RPC requests with session management - GET /mcp: SSE stream for server notifications - DELETE /mcp: Session termination - Origin validation (localhost-only by default) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,6 @@
|
||||
package mcp
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
@@ -11,7 +10,7 @@ import (
|
||||
"git.t-juice.club/torjus/labmcp/internal/database"
|
||||
)
|
||||
|
||||
// Server is an MCP server that handles JSON-RPC requests over stdio.
|
||||
// Server is an MCP server that handles JSON-RPC requests.
|
||||
type Server struct {
|
||||
store database.Store
|
||||
tools map[string]ToolHandler
|
||||
@@ -41,53 +40,34 @@ func (s *Server) registerTools() {
|
||||
// Tools will be implemented in handlers.go
|
||||
}
|
||||
|
||||
// Run starts the server, reading from r and writing to w.
|
||||
// Run starts the server using STDIO transport (backward compatibility).
|
||||
func (s *Server) Run(ctx context.Context, r io.Reader, w io.Writer) error {
|
||||
scanner := bufio.NewScanner(r)
|
||||
encoder := json.NewEncoder(w)
|
||||
transport := NewStdioTransport(s, r, w)
|
||||
return transport.Run(ctx)
|
||||
}
|
||||
|
||||
for scanner.Scan() {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
default:
|
||||
}
|
||||
|
||||
line := scanner.Bytes()
|
||||
if len(line) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
var req Request
|
||||
if err := json.Unmarshal(line, &req); err != nil {
|
||||
s.logger.Printf("Failed to parse request: %v", err)
|
||||
resp := Response{
|
||||
JSONRPC: "2.0",
|
||||
Error: &Error{
|
||||
Code: ParseError,
|
||||
Message: "Parse error",
|
||||
Data: err.Error(),
|
||||
},
|
||||
}
|
||||
if err := encoder.Encode(resp); err != nil {
|
||||
return fmt.Errorf("failed to write response: %w", err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
resp := s.handleRequest(ctx, &req)
|
||||
if resp != nil {
|
||||
if err := encoder.Encode(resp); err != nil {
|
||||
return fmt.Errorf("failed to write response: %w", err)
|
||||
}
|
||||
}
|
||||
// HandleMessage parses a JSON-RPC message and returns the response.
|
||||
// Returns (nil, nil) for notifications that don't require a response.
|
||||
func (s *Server) HandleMessage(ctx context.Context, data []byte) (*Response, error) {
|
||||
var req Request
|
||||
if err := json.Unmarshal(data, &req); err != nil {
|
||||
return &Response{
|
||||
JSONRPC: "2.0",
|
||||
Error: &Error{
|
||||
Code: ParseError,
|
||||
Message: "Parse error",
|
||||
Data: err.Error(),
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
if err := scanner.Err(); err != nil {
|
||||
return fmt.Errorf("scanner error: %w", err)
|
||||
}
|
||||
return s.HandleRequest(ctx, &req), nil
|
||||
}
|
||||
|
||||
return nil
|
||||
// HandleRequest processes a single request and returns a response.
|
||||
// Returns nil for notifications that don't require a response.
|
||||
func (s *Server) HandleRequest(ctx context.Context, req *Request) *Response {
|
||||
return s.handleRequest(ctx, req)
|
||||
}
|
||||
|
||||
// handleRequest processes a single request and returns a response.
|
||||
|
||||
Reference in New Issue
Block a user