Files
funstat-mcp-go/internal/transport/server.go
你的用户名 8d1ce4598d Initial commit: FunStat MCP Server Go implementation
- Telegram integration for customer statistics
- MCP server implementation with rate limiting
- Cache system for performance optimization
- Multi-language support
- RESTful API endpoints

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-04 15:28:06 +08:00

278 lines
6.3 KiB
Go

package transport
import (
"bytes"
"context"
"encoding/json"
"fmt"
"log"
"net/http"
"strings"
"sync"
"time"
"funstatmcp/internal/app"
)
type Server struct {
app *app.App
config app.Config
subscribersMu sync.Mutex
subscribers map[int]chan []byte
nextID int
}
func NewServer(appInstance *app.App, cfg app.Config) *Server {
return &Server{
app: appInstance,
config: cfg,
subscribers: make(map[int]chan []byte),
}
}
func (s *Server) Run(ctx context.Context) error {
mux := http.NewServeMux()
mux.HandleFunc("/sse", s.handleSSE)
mux.HandleFunc("/messages", s.handleMessages)
mux.HandleFunc("/health", s.handleHealth)
server := &http.Server{
Addr: fmt.Sprintf("%s:%d", s.config.Host, s.config.Port),
Handler: s.corsMiddleware(mux),
}
go func() {
<-ctx.Done()
shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := server.Shutdown(shutdownCtx); err != nil {
log.Printf("HTTP server shutdown error: %v", err)
}
}()
log.Printf("Funstat MCP Go server listening on http://%s:%d", s.config.Host, s.config.Port)
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
return err
}
return nil
}
func (s *Server) corsMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, X-MCP-Session-ID")
w.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS")
if r.Method == http.MethodOptions {
w.WriteHeader(http.StatusNoContent)
return
}
next.ServeHTTP(w, r)
})
}
func (s *Server) handleHealth(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
_ = json.NewEncoder(w).Encode(map[string]any{
"status": "ok",
"server": "funstat-mcp-go",
})
}
func (s *Server) handleSSE(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
w.Header().Set("Content-Type", "text/event-stream")
w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("Connection", "keep-alive")
flusher, ok := w.(http.Flusher)
if !ok {
http.Error(w, "streaming unsupported", http.StatusInternalServerError)
return
}
subscriber := make(chan []byte, 16)
id := s.addSubscriber(subscriber)
defer s.removeSubscriber(id)
heartbeatTicker := time.NewTicker(15 * time.Second)
defer heartbeatTicker.Stop()
ctx := r.Context()
for {
select {
case <-ctx.Done():
return
case <-heartbeatTicker.C:
fmt.Fprint(w, ": ping\n\n")
flusher.Flush()
case data := <-subscriber:
fmt.Fprintf(w, "event: message\n")
fmt.Fprintf(w, "data: %s\n\n", data)
flusher.Flush()
}
}
}
func (s *Server) addSubscriber(ch chan []byte) int {
s.subscribersMu.Lock()
defer s.subscribersMu.Unlock()
id := s.nextID
s.nextID++
s.subscribers[id] = ch
return id
}
func (s *Server) removeSubscriber(id int) {
s.subscribersMu.Lock()
defer s.subscribersMu.Unlock()
delete(s.subscribers, id)
}
func (s *Server) broadcast(payload []byte) {
s.subscribersMu.Lock()
defer s.subscribersMu.Unlock()
for _, ch := range s.subscribers {
select {
case ch <- payload:
default:
}
}
}
type jsonRPCRequest struct {
JSONRPC string `json:"jsonrpc"`
ID any `json:"id"`
Method string `json:"method"`
Params json.RawMessage `json:"params"`
}
type jsonRPCResponse struct {
JSONRPC string `json:"jsonrpc"`
ID any `json:"id"`
Result any `json:"result,omitempty"`
Error *jsonRPCError `json:"error,omitempty"`
}
type jsonRPCError struct {
Code int `json:"code"`
Message string `json:"message"`
}
func (s *Server) handleMessages(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
var request jsonRPCRequest
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
writeError(w, http.StatusBadRequest, fmt.Errorf("invalid JSON: %w", err))
return
}
response := s.handleRequest(r.Context(), request)
var buffer bytes.Buffer
if err := json.NewEncoder(&buffer).Encode(response); err != nil {
writeError(w, http.StatusInternalServerError, err)
return
}
payload := bytes.TrimSpace(buffer.Bytes())
s.broadcast(payload)
w.Header().Set("Content-Type", "application/json")
w.Write(payload)
}
func (s *Server) handleRequest(ctx context.Context, req jsonRPCRequest) jsonRPCResponse {
response := jsonRPCResponse{
JSONRPC: "2.0",
ID: req.ID,
}
if strings.TrimSpace(req.JSONRPC) != "2.0" {
response.Error = &jsonRPCError{Code: -32600, Message: "invalid jsonrpc version"}
return response
}
switch req.Method {
case "initialize":
response.Result = s.initializeResult()
case "list_tools":
response.Result = map[string]any{
"tools": app.ToolDefinitions(),
}
case "call_tool":
var payload struct {
Name string `json:"name"`
Arguments map[string]any `json:"arguments"`
}
if err := json.Unmarshal(req.Params, &payload); err != nil {
response.Error = &jsonRPCError{Code: -32602, Message: "invalid params"}
return response
}
if payload.Arguments == nil {
payload.Arguments = make(map[string]any)
}
result, err := s.app.CallTool(ctx, payload.Name, payload.Arguments)
if err != nil {
response.Result = map[string]any{
"content": []map[string]string{
{
"type": "text",
"text": fmt.Sprintf("❌ 错误: %s", err.Error()),
},
},
}
return response
}
response.Result = map[string]any{
"content": []map[string]string{
{
"type": "text",
"text": result,
},
},
}
default:
response.Error = &jsonRPCError{Code: -32601, Message: "method not found"}
}
return response
}
func (s *Server) initializeResult() map[string]any {
return map[string]any{
"protocolVersion": "2025-03-26",
"capabilities": map[string]any{
"tools": map[string]any{},
},
"serverInfo": map[string]any{
"name": "funstat-mcp-go",
"version": "1.0.0",
},
}
}
func writeError(w http.ResponseWriter, status int, err error) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
_ = json.NewEncoder(w).Encode(map[string]any{
"error": err.Error(),
})
}