feat(api): integrate MCP tools support in Ollama chat route

Add support for Model Context Protocol (MCP) tools in the Ollama chat API. Detect model capabilities to use native tools or prompt engineering. Handle tool calls, execute them via MCPExecutor, and inject results back into the conversation for seamless tool-assisted responses. This enhances chat functionality by enabling external tool integration.
This commit is contained in:
Evandro Fonseca Junior committed 2025-10-28 22:50:03 -03:00
1 parent 745746afa9
commit 4739a13458
18 files changed
+1782 -10

No files matched your search

+58
View File
@@ -0,0 +1,58 @@
/**
* API endpoint para listar MCPs disponíveis para uso no chat
*/
import { NextResponse } from "next/server";
import { MCPRepository } from "@/database/repositories/mcp";
import { MCPExecutor } from "@/lib/services/mcp-executor";
import type { MCPTool } from "@/lib/types/mcp-chat";
export const dynamic = "force-dynamic";
export async function GET() {
try {
// Buscar todos os MCPs instalados com status "ready"
const allMcps = MCPRepository.getAllInstalled();
const availableMcps = allMcps
.filter((mcp: any) => mcp.status === "ready")
.map((mcp: any) => {
const tools = MCPExecutor.getAvailableTools(mcp.id);
// Converter tools para formato OpenAI function calling
const toolsFormatted = tools.map((tool: MCPTool) => ({
type: "function" as const,
function: {
name: `${mcp.id}__${tool.name}`, // Prefixar com mcpId para evitar conflitos
description: tool.description,
parameters: tool.inputSchema,
},
}));
return {
mcpId: mcp.id,
name: mcp.name,
owner: mcp.owner,
repo: mcp.repo,
toolCount: tools.length,
tools: toolsFormatted,
rawTools: tools, // Tools originais para referência
};
});
return NextResponse.json({
success: true,
mcps: availableMcps,
count: availableMcps.length,
});
} catch (error: any) {
console.error("Error listing available MCPs:", error);
return NextResponse.json(
{
success: false,
error: error.message || "Failed to list available MCPs",
},
{ status: 500 }
);
}
}
+89
View File
@@ -0,0 +1,89 @@
/**
* API endpoint para executar um tool de MCP
*/
import { NextRequest, NextResponse } from "next/server";
import { MCPExecutor } from "@/lib/services/mcp-executor";
export const dynamic = "force-dynamic";
export async function POST(request: NextRequest) {
try {
const body = await request.json();
const { mcpId, toolName, parameters } = body;
// Validar parâmetros
if (!mcpId || !toolName) {
return NextResponse.json(
{
success: false,
error: "Missing mcpId or toolName",
},
{ status: 400 }
);
}
console.log(`📞 API call to execute tool: ${mcpId}.${toolName}`);
// Obter tools disponíveis
const availableTools = MCPExecutor.getAvailableTools(mcpId);
const tool = availableTools.find((t) => t.name === toolName);
if (!tool) {
return NextResponse.json(
{
success: false,
error: `Tool ${toolName} not found in MCP ${mcpId}`,
},
{ status: 404 }
);
}
// Validar parâmetros contra schema
const validation = MCPExecutor.validateParameters(parameters || {}, tool);
if (!validation.valid) {
return NextResponse.json(
{
success: false,
error: `Invalid parameters: ${validation.error}`,
},
{ status: 400 }
);
}
// Executar tool
const result = await MCPExecutor.executeMCPTool(
mcpId,
toolName,
parameters || {}
);
if (!result.success) {
return NextResponse.json(
{
success: false,
error: result.error,
executionTime: result.executionTime,
},
{ status: 500 }
);
}
return NextResponse.json({
success: true,
mcpId,
toolName,
result: result.result,
executionTime: result.executionTime,
});
} catch (error: any) {
console.error("Error executing MCP tool:", error);
return NextResponse.json(
{
success: false,
error: error.message || "Failed to execute MCP tool",
},
{ status: 500 }
);
}
}
+220 -5
View File
@@ -1,20 +1,75 @@
import { NextRequest } from "next/server";
import { chatWithStream } from "@/lib/ollama";
import { convertToOllamaMessages } from "@/lib/chat";
import { detectModelCapabilities } from "@/lib/services/model-capabilities";
import { MCPExecutor } from "@/lib/services/mcp-executor";
import {
generateToolsPrompt,
extractToolCallFromResponse,
formatToolResultForContext,
} from "@/lib/prompts/mcp-tools-prompt";
export const dynamic = "force-dynamic";
export async function POST(req: NextRequest) {
try {
const { model, messages, options, system } = await req.json();
const { model, messages, options, system, activeMcps } = await req.json();
if (!model || !messages?.length) {
return new Response("Missing model or messages", { status: 400 });
}
// Detectar capabilities do modelo
const capabilities = detectModelCapabilities(model);
// Preparar tools se MCPs ativos foram fornecidos
let tools: Array<any> = [];
let toolsPrompt = "";
const mcpToolsMap = new Map<string, { mcpId: string; toolName: string }>();
if (activeMcps && activeMcps.length > 0) {
console.log(`🔧 Active MCPs for chat: ${activeMcps.join(", ")}`);
// Buscar tools de todos os MCPs ativos
for (const mcpId of activeMcps) {
const mcpTools = MCPExecutor.getAvailableTools(mcpId);
for (const tool of mcpTools) {
const toolKey = `${mcpId}__${tool.name}`;
if (capabilities.supportsNativeTools) {
// Formato OpenAI para modelos com suporte nativo
tools.push({
type: "function",
function: {
name: toolKey,
description: tool.description,
parameters: tool.inputSchema,
},
});
}
// Mapa para lookup rápido
mcpToolsMap.set(toolKey, { mcpId, toolName: tool.name });
}
}
// Se modelo não suporta native tools, usar prompt engineering
if (!capabilities.supportsNativeTools) {
const allTools = Array.from(mcpToolsMap.entries()).map(
([key, { mcpId, toolName }]) => {
const mcpTools = MCPExecutor.getAvailableTools(mcpId);
return mcpTools.find((t) => t.name === toolName)!;
}
);
toolsPrompt = generateToolsPrompt(allTools);
}
}
const stream = new ReadableStream({
async start(controller) {
let isClosed = false;
let accumulatedResponse = "";
const safeEnqueue = (data: string) => {
if (!isClosed) {
@@ -40,17 +95,177 @@ export async function POST(req: NextRequest) {
try {
const ollamaMessages = convertToOllamaMessages(messages);
// Merge system prompt com tools prompt
const finalSystemPrompt = toolsPrompt
? `${system || ""}\n\n${toolsPrompt}`
: system;
// Preparar parâmetros para chat
const chatParams = capabilities.supportsNativeTools ? { tools } : {};
const response = await chatWithStream(
model,
ollamaMessages,
options,
system
finalSystemPrompt,
capabilities.supportsNativeTools ? tools : undefined
);
for await (const chunk of response) {
const token = chunk.message?.content || "";
if (token) {
safeEnqueue(JSON.stringify({ token }) + "\n");
// Verificar se há tool calls (modelos com suporte nativo)
if (
chunk.message?.tool_calls &&
chunk.message.tool_calls.length > 0
) {
console.log("🔧 Model requested tool calls (native)");
for (const toolCall of chunk.message.tool_calls) {
const toolKey = toolCall.function.name;
const toolInfo = mcpToolsMap.get(toolKey);
if (toolInfo) {
// Executar tool
const result = await MCPExecutor.executeMCPTool(
toolInfo.mcpId,
toolInfo.toolName,
toolCall.function.arguments || {}
);
if (result.success) {
// Enviar indicador de execução de tool
safeEnqueue(
JSON.stringify({
toolExecution: {
mcpId: toolInfo.mcpId,
toolName: toolInfo.toolName,
status: "success",
},
}) + "\n"
);
// Injetar resultado no contexto e continuar
const resultMessage = formatToolResultForContext(
toolInfo.toolName,
result.result
);
// Fazer nova chamada ao modelo com o resultado
const updatedMessages = [
...ollamaMessages,
{
role: "assistant",
content: chunk.message.content || "",
},
{ role: "system", content: resultMessage },
];
const followUpResponse = await chatWithStream(
model,
updatedMessages,
options,
finalSystemPrompt,
capabilities.supportsNativeTools ? tools : undefined
);
for await (const followUpChunk of followUpResponse) {
const token = followUpChunk.message?.content || "";
if (token) {
safeEnqueue(JSON.stringify({ token }) + "\n");
}
}
} else {
console.error("Tool execution failed:", result.error);
}
}
}
} else {
// Streaming normal de tokens
const token = chunk.message?.content || "";
if (token) {
accumulatedResponse += token;
safeEnqueue(JSON.stringify({ token }) + "\n");
}
}
}
// Se não há suporte nativo, verificar se resposta contém tool call via prompt engineering
if (!capabilities.supportsNativeTools && accumulatedResponse) {
const extractedToolCall =
extractToolCallFromResponse(accumulatedResponse);
if (extractedToolCall) {
console.log("🔧 Model requested tool call (prompt engineering)");
// Encontrar MCP e tool
let foundToolInfo: { mcpId: string; toolName: string } | null =
null;
for (const [key, info] of mcpToolsMap.entries()) {
if (
extractedToolCall.name === info.toolName ||
extractedToolCall.name === key
) {
foundToolInfo = info;
break;
}
}
if (foundToolInfo) {
// Executar tool
const result = await MCPExecutor.executeMCPTool(
foundToolInfo.mcpId,
foundToolInfo.toolName,
extractedToolCall.parameters
);
if (result.success) {
// Limpar resposta anterior (que continha o JSON)
safeEnqueue(JSON.stringify({ clearPrevious: true }) + "\n");
// Enviar indicador de execução
safeEnqueue(
JSON.stringify({
toolExecution: {
mcpId: foundToolInfo.mcpId,
toolName: foundToolInfo.toolName,
status: "success",
},
}) + "\n"
);
// Fazer nova chamada com resultado
const resultMessage = formatToolResultForContext(
foundToolInfo.toolName,
result.result
);
const updatedMessages = [
...ollamaMessages,
{
role: "assistant",
content: JSON.stringify({
tool_call: extractedToolCall,
}),
},
{ role: "system", content: resultMessage },
];
const followUpResponse = await chatWithStream(
model,
updatedMessages,
options,
finalSystemPrompt
);
for await (const followUpChunk of followUpResponse) {
const token = followUpChunk.message?.content || "";
if (token) {
safeEnqueue(JSON.stringify({ token }) + "\n");
}
}
}
}
}
}