Skip to content

Commit ee9f519

Browse files
author
team-coding-agent-1
committed
feat: Add MCP stdio tool injection support to /v1/chat/completions endpoint
- Import cogito and MCP tools packages - Add handleMCPToolExecution function to handle MCP tool execution - Integrate MCP check in ChatEndpoint before standard function calling - Support both stdio and remote MCP server configurations - Fall back to standard function calling when MCP is not configured or fails - Maintain backward compatibility with existing function calling behavior
1 parent bda826d commit ee9f519

1 file changed

Lines changed: 141 additions & 0 deletions

File tree

core/http/endpoints/openai/chat.go

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"fmt"
66
"strings"
77
"time"
8+
"net"
89

910
"github.com/google/uuid"
1011
"github.com/labstack/echo/v4"
@@ -19,6 +20,9 @@ import (
1920
pb "github.com/mudler/LocalAI/pkg/grpc/proto"
2021
"github.com/mudler/LocalAI/pkg/model"
2122

23+
"github.com/mudler/cogito"
24+
"github.com/mudler/cogito/clients"
25+
mcpTools "github.com/mudler/LocalAI/core/http/endpoints/mcp"
2226
"github.com/mudler/xlog"
2327
)
2428

@@ -401,6 +405,17 @@ func ChatEndpoint(cl *config.ModelConfigLoader, ml *model.ModelLoader, evaluator
401405

402406
xlog.Debug("Chat endpoint configuration read", "config", config)
403407

408+
409+
// Check for MCP configuration and handle if present
410+
mcpHandled, err := handleMCPToolExecution(c, input, config, startupOptions)
411+
if err != nil {
412+
xlog.Error("[MCP] Failed to handle MCP request", "error", err)
413+
return err
414+
}
415+
if mcpHandled {
416+
// MCP handled the request, return early
417+
return nil
418+
}
404419
funcs := input.Functions
405420
shouldUseFn := len(input.Functions) > 0 && config.ShouldUseFunctions()
406421
strictMode := false
@@ -866,6 +881,132 @@ func ChatEndpoint(cl *config.ModelConfigLoader, ml *model.ModelLoader, evaluator
866881
}
867882
}
868883

884+
// handleMCPToolExecution handles MCP tool execution using cogito when MCP config is set
885+
// It returns true if MCP was processed, false otherwise
886+
887+
// handleMCPToolExecution handles MCP tool execution using cogito when MCP config is set
888+
// It returns true if MCP was processed (and response sent), false otherwise
889+
func handleMCPToolExecution(
890+
ctx echo.Context,
891+
req *schema.OpenAIRequest,
892+
config *config.ModelConfig,
893+
appConfig *config.ApplicationConfig,
894+
) (bool, error) {
895+
// Check if MCP is configured
896+
if config.MCP.Stdio.Servers == nil || len(config.MCP.Stdio.Servers) == 0 {
897+
if config.MCP.Servers.Servers == nil || len(config.MCP.Servers.Servers) == 0 {
898+
return false, nil
899+
}
900+
}
901+
902+
xlog.Debug("[MCP] MCP configuration detected, initializing sessions", "model", config.Name)
903+
904+
// Get MCP sessions from config
905+
sessions, err := mcpTools.SessionsFromMCPConfig(config.Name, config.MCP.Servers, config.MCP.Stdio)
906+
if err != nil {
907+
xlog.Error("[MCP] Failed to create MCP sessions", "error", err)
908+
// Fall back to standard processing
909+
return false, nil
910+
}
911+
912+
if len(sessions) == 0 {
913+
xlog.Warn("[MCP] No working MCP servers found, falling back to standard function calling")
914+
return false, nil
915+
}
916+
917+
xlog.Debug("[MCP] Connected to MCP servers", "count", len(sessions))
918+
919+
// Get API address and key for cogito LLM client
920+
apiHost, apiPort, err := net.SplitHostPort(appConfig.APIAddress)
921+
if err != nil {
922+
xlog.Error("[MCP] Failed to parse API address", "error", err)
923+
return false, nil
924+
}
925+
apiKey := ""
926+
if len(appConfig.ApiKeys) > 0 {
927+
apiKey = appConfig.ApiKeys[0]
928+
}
929+
930+
// Build fragment from chat messages
931+
fragment := cogito.NewEmptyFragment()
932+
for _, message := range req.Messages {
933+
fragment = fragment.AddMessage(cogito.MessageRole(message.Role), message.StringContent)
934+
}
935+
936+
// Create OpenAI LLM client for cogito
937+
llm := clients.NewLocalAILLM(config.Name, apiKey, "http://"+apiHost+":"+apiPort)
938+
939+
// Build cogito options
940+
cogitoOpts := config.BuildCogitoOptions()
941+
cogitoOpts = append(cogitoOpts,
942+
cogito.WithContext(ctx.Request().Context()),
943+
cogito.WithMCPs(sessions...),
944+
cogito.WithStatusCallback(func(s string) {
945+
xlog.Debug("[MCP Chat] Status", "model", config.Name, "status", s)
946+
}),
947+
cogito.WithReasoningCallback(func(s string) {
948+
xlog.Debug("[MCP Chat] Reasoning", "model", config.Name, "reasoning", s)
949+
}),
950+
cogito.WithToolCallBack(func(t *cogito.ToolChoice, state *cogito.SessionState) cogito.ToolCallDecision {
951+
xlog.Debug("[MCP Chat] Tool call", "model", config.Name, "tool", t.Name, "reasoning", t.Reasoning, "arguments", t.Arguments)
952+
return cogito.ToolCallDecision{Approved: true}
953+
}),
954+
cogito.WithToolCallResultCallback(func(t cogito.ToolStatus) {
955+
xlog.Debug("[MCP Chat] Tool result", "model", config.Name, "tool", t.Name, "result", t.Result)
956+
}),
957+
)
958+
959+
// Execute tools via cogito
960+
resultFragment, err := cogito.ExecuteTools(llm, fragment, cogitoOpts...)
961+
if err != nil {
962+
if err == cogito.ErrNoToolSelected {
963+
// No tools were selected, fall back to standard processing
964+
xlog.Debug("[MCP] No tools selected, falling back to standard function calling")
965+
return false, nil
966+
}
967+
xlog.Error("[MCP] Tool execution failed", "error", err)
968+
// Fall back to standard processing
969+
return false, nil
970+
}
971+
972+
// Get the assistant's response from the fragment
973+
lastMsg := resultFragment.LastMessage()
974+
if lastMsg == nil || lastMsg.Content == "" {
975+
xlog.Debug("[MCP] No message content in fragment, falling back to standard function calling")
976+
return false, nil
977+
}
978+
979+
xlog.Debug("[MCP] MCP processing complete, response generated", "model", config.Name, "content_length", len(lastMsg.Content))
980+
981+
// Build response with the assistant's message
982+
content := lastMsg.Content
983+
stopReason := FinishReasonStop
984+
choice := schema.Choice{
985+
FinishReason: &stopReason,
986+
Message: &schema.Message{
987+
Role: "assistant",
988+
Content: &content,
989+
},
990+
}
991+
992+
// Include reasoning if present
993+
if lastMsg.Reasoning != "" {
994+
choice.Message.Reasoning = &lastMsg.Reasoning
995+
}
996+
997+
resp := &schema.OpenAIResponse{
998+
ID: uuid.New().String(),
999+
Created: int(time.Now().Unix()),
1000+
Model: req.Model,
1001+
Choices: []schema.Choice{choice},
1002+
Object: "chat.completion",
1003+
}
1004+
1005+
respData, _ := json.Marshal(resp)
1006+
xlog.Debug("[MCP] Response", "response", string(respData))
1007+
1008+
return ctx.JSON(200, resp) == nil, nil
1009+
}
8691010
func handleQuestion(config *config.ModelConfig, funcResults []functions.FuncCallResults, result, prompt string) (string, error) {
8701011

8711012
if len(funcResults) == 0 && result != "" {

0 commit comments

Comments
 (0)