// Package mcp implements an MCP HTTP handler for the ingestion service. // Exposed tools: brain_query, brain_write, brain_ingest, brain_ingest_raw, session_log. package mcp import ( "context" "encoding/json" "fmt" "net/http" "github.com/mathiasbq/hyperguild/ingestion/internal/pipeline" ) type request struct { JSONRPC string `json:"jsonrpc"` ID any `json:"id"` Method string `json:"method"` Params json.RawMessage `json:"params"` } type response struct { JSONRPC string `json:"jsonrpc"` ID any `json:"id,omitempty"` Result any `json:"result,omitempty"` Error *rpcError `json:"error,omitempty"` } type rpcError struct { Code int `json:"code"` Message string `json:"message"` } // Server handles MCP JSON-RPC over HTTP for the ingestion service. type Server struct { brainDir string pipeline pipeline.Config llm pipeline.CompleteFunc } // NewServer constructs a Server bound to brainDir. pipelineCfg supplies the // LLM-backed pipeline; llm may be nil for non-LLM tools only. func NewServer(brainDir string, pipelineCfg *pipeline.Config, llm pipeline.CompleteFunc) *Server { cfg := pipeline.Config{} if pipelineCfg != nil { cfg = *pipelineCfg } return &Server{brainDir: brainDir, pipeline: cfg, llm: llm} } func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { var req request if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeError(w, nil, -32700, "parse error") return } // JSON-RPC 2.0 notifications (no id) must not receive a response. if req.ID == nil { return } var result any var rpcErr *rpcError switch req.Method { case "initialize": result = map[string]any{ "protocolVersion": "2024-11-05", "capabilities": map[string]any{"tools": map[string]any{}}, "serverInfo": map[string]any{"name": "ingestion-brain", "version": "0.1.0"}, } case "tools/list": result = map[string]any{"tools": s.tools()} case "tools/call": var p struct { Name string `json:"name"` Arguments json.RawMessage `json:"arguments"` } if err := json.Unmarshal(req.Params, &p); err != nil { rpcErr = &rpcError{Code: -32602, Message: "invalid params"} break } out, err := s.handleCall(r.Context(), p.Name, p.Arguments) if err != nil { rpcErr = &rpcError{Code: -32000, Message: err.Error()} break } result = map[string]any{ "content": []map[string]any{{"type": "text", "text": string(out)}}, } default: rpcErr = &rpcError{Code: -32601, Message: "method not found: " + req.Method} } w.Header().Set("Content-Type", "application/json") _ = json.NewEncoder(w).Encode(response{ JSONRPC: "2.0", ID: req.ID, Result: result, Error: rpcErr, }) } func writeError(w http.ResponseWriter, id any, code int, msg string) { w.Header().Set("Content-Type", "application/json") _ = json.NewEncoder(w).Encode(response{ JSONRPC: "2.0", ID: id, Error: &rpcError{Code: code, Message: msg}, }) } // handleCall dispatches a tools/call to the appropriate tool handler. func (s *Server) handleCall(ctx context.Context, name string, args json.RawMessage) (json.RawMessage, error) { switch name { case "brain_query": return s.brainQuery(ctx, args) default: return nil, fmt.Errorf("unknown tool: %s", name) } }