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 { NextRequest } from "next/server";
import { chatWithStream } from "@/lib/ollama"; import { chatWithStream } from "@/lib/ollama";
import { convertToOllamaMessages } from "@/lib/chat"; 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 const dynamic = "force-dynamic";
export async function POST(req: NextRequest) { export async function POST(req: NextRequest) {
try { try {
const { model, messages, options, system } = await req.json(); const { model, messages, options, system, activeMcps } = await req.json();
if (!model || !messages?.length) { if (!model || !messages?.length) {
return new Response("Missing model or messages", { status: 400 }); 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({ const stream = new ReadableStream({
async start(controller) { async start(controller) {
let isClosed = false; let isClosed = false;
let accumulatedResponse = "";
const safeEnqueue = (data: string) => { const safeEnqueue = (data: string) => {
if (!isClosed) { if (!isClosed) {
@@ -40,17 +95,177 @@ export async function POST(req: NextRequest) {
try { try {
const ollamaMessages = convertToOllamaMessages(messages); 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( const response = await chatWithStream(
model, model,
ollamaMessages, ollamaMessages,
options, options,
system finalSystemPrompt,
capabilities.supportsNativeTools ? tools : undefined
); );
for await (const chunk of response) { for await (const chunk of response) {
const token = chunk.message?.content || ""; // Verificar se há tool calls (modelos com suporte nativo)
if (token) { if (
safeEnqueue(JSON.stringify({ token }) + "\n"); 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");
}
}
}
}
} }
} }
+99 -1
View File
@@ -1,10 +1,11 @@
"use client"; "use client";
import { useState, useEffect, useRef } from "react"; import { useState, useEffect, useRef, useCallback } from "react";
import { ModelDropdown } from "@/components/model/ModelDropdown"; import { ModelDropdown } from "@/components/model/ModelDropdown";
import { ModelPullDialog } from "@/components/model/ModelPullDialog"; import { ModelPullDialog } from "@/components/model/ModelPullDialog";
import { ChatMessage } from "@/components/chat/ChatMessage"; import { ChatMessage } from "@/components/chat/ChatMessage";
import { SettingsModal } from "@/components/chat/SettingsModal"; import { SettingsModal } from "@/components/chat/SettingsModal";
import { MCPSelector, ActiveMCPBadges } from "@/components/chat/MCPSelector";
import { Send, Square } from "lucide-react"; import { Send, Square } from "lucide-react";
import type { ModelInfo } from "@/lib/models"; import type { ModelInfo } from "@/lib/models";
import type { Message } from "@/lib/chat"; import type { Message } from "@/lib/chat";
@@ -37,6 +38,10 @@ export function ChatContainer({
const [systemPrompt, setSystemPrompt] = useState<string>(""); const [systemPrompt, setSystemPrompt] = useState<string>("");
const messagesEndRef = useRef<HTMLDivElement>(null); const messagesEndRef = useRef<HTMLDivElement>(null);
// Estado para MCPs ativos
const [activeMcps, setActiveMcps] = useState<string[]>([]);
const [mcpNames, setMcpNames] = useState<Map<string, string>>(new Map());
// Carregar modelo salvo ao iniciar // Carregar modelo salvo ao iniciar
useEffect(() => { useEffect(() => {
if (ready && prefs.selectedModel && !selectedModel) { if (ready && prefs.selectedModel && !selectedModel) {
@@ -61,6 +66,69 @@ export function ChatContainer({
update({ selectedModel: modelName }); // Salvar no Redis update({ selectedModel: modelName }); // Salvar no Redis
}; };
// Carregar MCPs ativos do localStorage
useEffect(() => {
const saved = localStorage.getItem("ollahub-active-mcps");
if (saved) {
try {
const parsed = JSON.parse(saved);
setActiveMcps(parsed.mcps || []);
setMcpNames(new Map(parsed.names || []));
} catch (e) {
console.error("Error loading active MCPs:", e);
}
}
}, []);
// Salvar MCPs ativos no localStorage quando mudarem
useEffect(() => {
if (activeMcps.length > 0 || mcpNames.size > 0) {
localStorage.setItem(
"ollahub-active-mcps",
JSON.stringify({
mcps: activeMcps,
names: Array.from(mcpNames.entries()),
})
);
}
}, [activeMcps, mcpNames]);
// Handlers para MCP Selector
const handleToggleMCP = useCallback(
async (mcpId: string) => {
setActiveMcps((prev) => {
if (prev.includes(mcpId)) {
return prev.filter((id) => id !== mcpId);
} else {
// Buscar nome do MCP se ainda não temos
if (!mcpNames.has(mcpId)) {
fetch("/api/mcp/available-for-chat")
.then((res) => res.json())
.then((data) => {
if (data.success) {
const mcp = data.mcps.find((m: any) => m.mcpId === mcpId);
if (mcp) {
setMcpNames((prev) => new Map(prev).set(mcpId, mcp.name));
}
}
})
.catch(console.error);
}
return [...prev, mcpId];
}
});
},
[mcpNames]
);
const handleClearAllMCPs = useCallback(() => {
setActiveMcps([]);
}, []);
const handleRemoveMCP = useCallback((mcpId: string) => {
setActiveMcps((prev) => prev.filter((id) => id !== mcpId));
}, []);
const scrollToBottom = () => { const scrollToBottom = () => {
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
}; };
@@ -159,6 +227,7 @@ export function ChatContainer({
messages: messagesToSend, messages: messagesToSend,
options, options,
system: systemPrompt || undefined, system: systemPrompt || undefined,
activeMcps: activeMcps.length > 0 ? activeMcps : undefined,
}), }),
signal: abortRef.current.signal, signal: abortRef.current.signal,
}); });
@@ -187,6 +256,21 @@ export function ChatContainer({
try { try {
const data = JSON.parse(line); const data = JSON.parse(line);
// Tool execution indicator
if (data.toolExecution) {
console.log(
`🔧 MCP Tool executed: ${data.toolExecution.mcpId}.${data.toolExecution.toolName}`
);
// Opcional: Mostrar indicador visual
}
// Clear previous response (para prompt engineering)
if (data.clearPrevious) {
assistantContent = "";
setStreamingContent("");
}
if (data.token) { if (data.token) {
assistantContent += data.token; assistantContent += data.token;
setStreamingContent(assistantContent); setStreamingContent(assistantContent);
@@ -411,7 +495,21 @@ export function ChatContainer({
{/* Input */} {/* Input */}
<div className="flex-shrink-0 border-t border-[var(--border)] bg-[var(--background)]"> <div className="flex-shrink-0 border-t border-[var(--border)] bg-[var(--background)]">
<form onSubmit={handleSubmit} className="p-4"> <form onSubmit={handleSubmit} className="p-4">
{/* Active MCP Badges */}
<ActiveMCPBadges
activeMcps={activeMcps}
mcpNames={mcpNames}
onRemove={handleRemoveMCP}
/>
<div className="flex gap-2"> <div className="flex gap-2">
{/* MCP Selector */}
<MCPSelector
activeMcps={activeMcps}
onToggleMCP={handleToggleMCP}
onClearAll={handleClearAllMCPs}
/>
<textarea <textarea
value={inputContent} value={inputContent}
onChange={(e) => setInputContent(e.target.value)} onChange={(e) => setInputContent(e.target.value)}
+246
View File
@@ -0,0 +1,246 @@
"use client";
import { useState, useEffect, useRef } from "react";
import { Plug, ChevronDown, X, Zap } from "lucide-react";
interface MCPForChat {
mcpId: string;
name: string;
toolCount: number;
tools: Array<{ name: string; description: string }>;
}
interface MCPSelectorProps {
activeMcps: string[];
onToggleMCP: (mcpId: string) => void;
onClearAll: () => void;
}
export function MCPSelector({
activeMcps,
onToggleMCP,
onClearAll,
}: MCPSelectorProps) {
const [isOpen, setIsOpen] = useState(false);
const [availableMcps, setAvailableMcps] = useState<MCPForChat[]>([]);
const [loading, setLoading] = useState(false);
const dropdownRef = useRef<HTMLDivElement>(null);
// Carregar MCPs disponíveis
useEffect(() => {
if (isOpen) {
loadAvailableMCPs();
}
}, [isOpen]);
// Fechar dropdown ao clicar fora
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (
dropdownRef.current &&
!dropdownRef.current.contains(event.target as Node)
) {
setIsOpen(false);
}
};
if (isOpen) {
document.addEventListener("mousedown", handleClickOutside);
return () => {
document.removeEventListener("mousedown", handleClickOutside);
};
}
}, [isOpen]);
const loadAvailableMCPs = async () => {
setLoading(true);
try {
const response = await fetch("/api/mcp/available-for-chat");
const data = await response.json();
if (data.success) {
setAvailableMcps(
data.mcps.map((mcp: any) => ({
mcpId: mcp.mcpId,
name: mcp.name,
toolCount: mcp.toolCount,
tools: mcp.rawTools || [],
}))
);
}
} catch (error) {
console.error("Error loading available MCPs:", error);
} finally {
setLoading(false);
}
};
const handleToggle = (mcpId: string) => {
onToggleMCP(mcpId);
};
return (
<div className="relative" ref={dropdownRef}>
{/* Button */}
<button
onClick={() => setIsOpen(!isOpen)}
className="flex items-center gap-2 px-3 py-2 rounded-lg border border-[var(--border)] hover:bg-[var(--surface)] transition-colors"
title="Manage MCP Tools"
>
<Plug className="h-4 w-4" />
{activeMcps.length > 0 && (
<span className="flex items-center justify-center h-5 min-w-[20px] px-1.5 bg-[var(--primary)] text-[var(--primary-foreground)] text-xs font-medium rounded-full">
{activeMcps.length}
</span>
)}
<ChevronDown
className={`h-4 w-4 transition-transform ${
isOpen ? "rotate-180" : ""
}`}
/>
</button>
{/* Dropdown */}
{isOpen && (
<div className="absolute bottom-full left-0 mb-2 w-80 bg-[var(--background)] border border-[var(--border)] rounded-lg shadow-lg z-50 max-h-96 overflow-hidden flex flex-col">
{/* Header */}
<div className="p-3 border-b border-[var(--border)] flex items-center justify-between">
<div className="flex items-center gap-2">
<Plug className="h-4 w-4" />
<span className="font-semibold text-sm">MCP Tools</span>
</div>
{activeMcps.length > 0 && (
<button
onClick={onClearAll}
className="text-xs text-red-500 hover:text-red-600 font-medium"
>
Clear All
</button>
)}
</div>
{/* Content */}
<div className="overflow-y-auto flex-1">
{loading ? (
<div className="p-4 text-center text-sm text-[var(--muted-foreground)]">
Loading MCPs...
</div>
) : availableMcps.length === 0 ? (
<div className="p-4 text-center text-sm text-[var(--muted-foreground)]">
No MCPs installed
</div>
) : (
<div className="p-2">
{availableMcps.map((mcp) => {
const isActive = activeMcps.includes(mcp.mcpId);
return (
<div
key={mcp.mcpId}
className={`p-3 rounded-md mb-2 cursor-pointer transition-colors ${
isActive
? "bg-[var(--primary)]/10 border border-[var(--primary)]/30"
: "hover:bg-[var(--surface)]"
}`}
onClick={() => handleToggle(mcp.mcpId)}
>
<div className="flex items-start justify-between">
<div className="flex-1">
<div className="flex items-center gap-2 mb-1">
<input
type="checkbox"
checked={isActive}
onChange={() => {}}
className="rounded"
/>
<span className="font-medium text-sm">
{mcp.name}
</span>
</div>
<div className="flex items-center gap-2 text-xs text-[var(--muted-foreground)] ml-6">
<Zap className="h-3 w-3" />
<span>{mcp.toolCount} tools available</span>
</div>
{isActive && mcp.tools.length > 0 && (
<div className="mt-2 ml-6">
<div className="flex flex-wrap gap-1">
{mcp.tools.slice(0, 3).map((tool, idx) => (
<span
key={idx}
className="text-[10px] px-1.5 py-0.5 bg-[var(--primary)]/20 text-[var(--primary)] rounded"
title={tool.description}
>
{tool.name}
</span>
))}
{mcp.tools.length > 3 && (
<span className="text-[10px] px-1.5 py-0.5 bg-[var(--surface)] rounded">
+{mcp.tools.length - 3}
</span>
)}
</div>
</div>
)}
</div>
</div>
</div>
);
})}
</div>
)}
</div>
{/* Footer info */}
{activeMcps.length > 0 && (
<div className="p-3 border-t border-[var(--border)] bg-[var(--surface)]/50">
<p className="text-xs text-[var(--muted-foreground)]">
AI can use {activeMcps.length} MCP
{activeMcps.length > 1 ? "s" : ""} to help answer your questions
</p>
</div>
)}
</div>
)}
</div>
);
}
/**
* Pills/badges component to show active MCPs in the chat input area
*/
interface ActiveMCPBadgesProps {
activeMcps: string[];
mcpNames: Map<string, string>;
onRemove: (mcpId: string) => void;
}
export function ActiveMCPBadges({
activeMcps,
mcpNames,
onRemove,
}: ActiveMCPBadgesProps) {
if (activeMcps.length === 0) return null;
return (
<div className="flex flex-wrap gap-1.5 mb-2">
{activeMcps.map((mcpId) => (
<div
key={mcpId}
className="flex items-center gap-1 px-2 py-1 bg-[var(--primary)]/10 text-[var(--primary)] text-xs rounded-md border border-[var(--primary)]/30"
>
<Zap className="h-3 w-3" />
<span className="font-medium">{mcpNames.get(mcpId) || mcpId}</span>
<button
onClick={(e) => {
e.stopPropagation();
onRemove(mcpId);
}}
className="ml-1 hover:bg-[var(--primary)]/20 rounded"
>
<X className="h-3 w-3" />
</button>
</div>
))}
</div>
);
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+156
View File
@@ -0,0 +1,156 @@
/**
* Repositório para gerenciar MCPs ativos por conversa/usuário
*/
import Database from "better-sqlite3";
import path from "path";
const dbPath = path.join(process.cwd(), "database", "ollahub.db");
const db = new Database(dbPath);
export interface ActiveMCP {
id: string;
conversation_id: string;
mcp_id: string;
enabled: boolean;
config?: any;
created_at: string;
}
export class MCPActiveRepository {
/**
* Salvar MCPs ativos para uma conversa
* @param conversationId ID da conversa
* @param mcpIds Array de IDs de MCPs ativos
*/
static saveActiveMCPs(conversationId: string, mcpIds: string[]): void {
try {
// Primeiro, remover todas as entradas antigas da conversa
const deleteStmt = db.prepare(
"DELETE FROM active_mcps WHERE conversation_id = ?"
);
deleteStmt.run(conversationId);
// Inserir novos MCPs ativos
const insertStmt = db.prepare(`
INSERT INTO active_mcps (conversation_id, mcp_id, enabled, created_at)
VALUES (?, ?, 1, CURRENT_TIMESTAMP)
`);
for (const mcpId of mcpIds) {
insertStmt.run(conversationId, mcpId);
}
console.log(
`Saved ${mcpIds.length} active MCPs for conversation ${conversationId}`
);
} catch (error) {
console.error("Error saving active MCPs:", error);
throw error;
}
}
/**
* Obter MCPs ativos de uma conversa
* @param conversationId ID da conversa
* @returns Array de IDs de MCPs ativos
*/
static getActiveMCPs(conversationId: string): string[] {
try {
const stmt = db.prepare(`
SELECT mcp_id FROM active_mcps
WHERE conversation_id = ? AND enabled = 1
ORDER BY created_at DESC
`);
const results = stmt.all(conversationId) as { mcp_id: string }[];
return results.map((row) => row.mcp_id);
} catch (error) {
console.error("Error getting active MCPs:", error);
return [];
}
}
/**
* Ativar/desativar um MCP específico para uma conversa
* @param conversationId ID da conversa
* @param mcpId ID do MCP
* @param enabled true para ativar, false para desativar
*/
static toggleMCP(
conversationId: string,
mcpId: string,
enabled: boolean
): void {
try {
// Verificar se já existe
const checkStmt = db.prepare(`
SELECT id FROM active_mcps
WHERE conversation_id = ? AND mcp_id = ?
`);
const existing = checkStmt.get(conversationId, mcpId);
if (existing) {
// Atualizar
const updateStmt = db.prepare(`
UPDATE active_mcps SET enabled = ?
WHERE conversation_id = ? AND mcp_id = ?
`);
updateStmt.run(enabled ? 1 : 0, conversationId, mcpId);
} else {
// Inserir
const insertStmt = db.prepare(`
INSERT INTO active_mcps (conversation_id, mcp_id, enabled, created_at)
VALUES (?, ?, ?, CURRENT_TIMESTAMP)
`);
insertStmt.run(conversationId, mcpId, enabled ? 1 : 0);
}
console.log(
`MCP ${mcpId} ${
enabled ? "enabled" : "disabled"
} for conversation ${conversationId}`
);
} catch (error) {
console.error("Error toggling MCP:", error);
throw error;
}
}
/**
* Limpar MCPs ativos de uma conversa
* @param conversationId ID da conversa
*/
static clearActiveMCPs(conversationId: string): void {
try {
const stmt = db.prepare(
"DELETE FROM active_mcps WHERE conversation_id = ?"
);
stmt.run(conversationId);
console.log(`Cleared active MCPs for conversation ${conversationId}`);
} catch (error) {
console.error("Error clearing active MCPs:", error);
throw error;
}
}
/**
* Obter todas as conversas que usam um MCP específico
* @param mcpId ID do MCP
* @returns Array de IDs de conversas
*/
static getConversationsUsingMCP(mcpId: string): string[] {
try {
const stmt = db.prepare(`
SELECT DISTINCT conversation_id FROM active_mcps
WHERE mcp_id = ? AND enabled = 1
`);
const results = stmt.all(mcpId) as { conversation_id: string }[];
return results.map((row) => row.conversation_id);
} catch (error) {
console.error("Error getting conversations using MCP:", error);
return [];
}
}
}
+32
View File
@@ -182,6 +182,38 @@ export class MCPRepository {
} }
} }
/**
* Obter todos os MCPs instalados com informações completas (incluindo status, environment, etc)
*/
static getAllInstalled(): any[] {
try {
const stmt = db.prepare(`
SELECT
id, owner, repo, name, config, tools,
status, status_message,
environment_path, executable_command,
validation_result, install_logs,
installed_at, updated_at
FROM mcp_installations
ORDER BY installed_at DESC
`);
const results = stmt.all() as any[];
return results.map((row) => ({
...row,
config: row.config ? JSON.parse(row.config) : null,
tools: row.tools ? JSON.parse(row.tools) : [],
validation_result: row.validation_result
? JSON.parse(row.validation_result)
: null,
install_logs: row.install_logs ? JSON.parse(row.install_logs) : [],
}));
} catch (error) {
console.error("Error getting all installed MCPs:", error);
return [];
}
}
/** /**
* Atualizar timestamp de um MCP instalado * Atualizar timestamp de um MCP instalado
*/ */
+14
View File
@@ -73,6 +73,20 @@ CREATE TABLE IF NOT EXISTS mcp_cache_metadata (
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
); );
-- Tabela para rastrear MCPs ativos por conversa
CREATE TABLE IF NOT EXISTS active_mcps (
id INTEGER PRIMARY KEY AUTOINCREMENT,
conversation_id TEXT NOT NULL,
mcp_id TEXT NOT NULL,
enabled INTEGER DEFAULT 1,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (conversation_id) REFERENCES conversations(id) ON DELETE CASCADE,
UNIQUE(conversation_id, mcp_id)
);
CREATE INDEX IF NOT EXISTS idx_active_mcps_conversation ON active_mcps(conversation_id);
CREATE INDEX IF NOT EXISTS idx_active_mcps_mcp ON active_mcps(mcp_id);
-- Índices para performance -- Índices para performance
CREATE INDEX idx_messages_conversation ON messages(conversation_id, timestamp); CREATE INDEX idx_messages_conversation ON messages(conversation_id, timestamp);
CREATE INDEX idx_conversations_updated ON conversations(updated_at DESC); CREATE INDEX idx_conversations_updated ON conversations(updated_at DESC);
+13 -4
View File
@@ -24,17 +24,26 @@ export async function chatWithStream(
model: string, model: string,
messages: Array<{ role: string; content: string }>, messages: Array<{ role: string; content: string }>,
options?: Record<string, unknown>, options?: Record<string, unknown>,
system?: string system?: string,
tools?: Array<any>
) { ) {
const client = new Ollama({ host: OLLAMA_HOST }); const client = new Ollama({ host: OLLAMA_HOST });
return client.chat({
const chatOptions: any = {
model, model,
messages: system messages: system
? [...messages, { role: "system", content: system }] ? [{ role: "system", content: system }, ...messages]
: messages, : messages,
stream: true, stream: true,
options, options,
}); };
// Adicionar tools se fornecidos
if (tools && tools.length > 0) {
chatOptions.tools = tools;
}
return client.chat(chatOptions);
} }
export async function pullModel(model: string) { export async function pullModel(model: string) {
+195
View File
@@ -0,0 +1,195 @@
/**
* Templates de prompt engineering para modelos sem suporte nativo a function calling
*/
import type { MCPTool } from "@/lib/types/mcp-chat";
/**
* Gera prompt de instruções para uso de tools via prompt engineering
* @param tools Array de tools disponíveis
* @returns String com instruções formatadas
*/
export function generateToolsPrompt(tools: MCPTool[]): string {
if (tools.length === 0) {
return "";
}
const toolsDescription = tools
.map((tool, index) => {
const params = Object.entries(tool.inputSchema.properties || {})
.map(([name, schema]: [string, any]) => {
const required = tool.inputSchema.required?.includes(name)
? " (required)"
: " (optional)";
const type = schema.type || "any";
const desc = schema.description || "";
return ` - ${name}: ${type}${required}${desc ? ` - ${desc}` : ""}`;
})
.join("\n");
return `${index + 1}. **${tool.name}**
Description: ${tool.description}
Parameters:
${params || " (no parameters)"}`;
})
.join("\n\n");
return `
# FERRAMENTAS DISPONÍVEIS
Você tem acesso às seguintes ferramentas externas para auxiliar o usuário:
${toolsDescription}
## INSTRUÇÕES IMPORTANTES
1. **Quando usar ferramentas:**
- Use ferramentas quando precisar de informações em tempo real
- Use ferramentas quando precisar executar ações externas
- Use ferramentas quando o usuário solicitar dados específicos
2. **Como usar ferramentas:**
- Para chamar uma ferramenta, responda EXATAMENTE no seguinte formato JSON:
\`\`\`json
{
"tool_call": {
"name": "nome_da_ferramenta",
"parameters": {
"parametro1": "valor1",
"parametro2": "valor2"
}
}
}
\`\`\`
- NÃO adicione texto antes ou depois do JSON
- Certifique-se de que o JSON está bem formatado
- Use apenas ferramentas da lista acima
3. **REGRA CRUCIAL - NUNCA MENCIONE AO USUÁRIO:**
- NUNCA mencione que você está usando ferramentas
- NUNCA mencione MCPs ou APIs externas
- NUNCA diga "vou usar a ferramenta X"
- NUNCA explique o processo técnico de como obteve informações
- Apresente as informações naturalmente como se fossem seu conhecimento
4. **Após receber resultado da ferramenta:**
- Você receberá uma mensagem do sistema com o resultado
- Integre o resultado naturalmente na sua resposta
- Responda ao usuário de forma fluida e natural
- Cite informações específicas do resultado quando relevante
## EXEMPLO
Usuário: "Qual é o clima em São Paulo?"
Sua resposta:
\`\`\`json
{
"tool_call": {
"name": "get_weather",
"parameters": {
"city": "São Paulo"
}
}
}
\`\`\`
[Sistema retorna: {"temperature": 25, "condition": "sunny"}]
Sua resposta final ao usuário:
"O clima em São Paulo está ensolarado com 25°C."
**NÃO faça:**
"Vou consultar a ferramenta de clima para você..."
"Usei o get_weather e obtive..."
"De acordo com a API..."
`;
}
/**
* Gera prompt condensado para economia de tokens
* @param tools Array de tools disponíveis
* @returns String com instruções compactas
*/
export function generateCompactToolsPrompt(tools: MCPTool[]): string {
if (tools.length === 0) {
return "";
}
const toolsList = tools
.map((tool) => {
const params = Object.keys(tool.inputSchema.properties || {}).join(", ");
return `${tool.name}(${params}): ${tool.description}`;
})
.join("\n");
return `
TOOLS: ${tools.length} available
${toolsList}
FORMAT: {"tool_call":{"name":"tool_name","parameters":{...}}}
RULE: Never mention tools to user. Present results naturally.
`;
}
/**
* Extrai tool call de uma resposta do modelo
* @param response Resposta do modelo
* @returns Tool call extraído ou null
*/
export function extractToolCallFromResponse(response: string): {
name: string;
parameters: Record<string, any>;
} | null {
try {
// Tentar extrair JSON da resposta
const jsonMatch = response.match(/```json\s*([\s\S]*?)\s*```/);
const jsonText = jsonMatch ? jsonMatch[1] : response;
const parsed = JSON.parse(jsonText.trim());
if (parsed.tool_call && parsed.tool_call.name) {
return {
name: parsed.tool_call.name,
parameters: parsed.tool_call.parameters || {},
};
}
return null;
} catch (error) {
// Tentar extrair manualmente
const nameMatch = response.match(/"name":\s*"([^"]+)"/);
if (nameMatch) {
const name = nameMatch[1];
const paramsMatch = response.match(/"parameters":\s*({[^}]+})/);
const parameters = paramsMatch ? JSON.parse(paramsMatch[1]) : {};
return { name, parameters };
}
return null;
}
}
/**
* Formata resultado de tool para injetar no contexto
* @param toolName Nome do tool
* @param result Resultado da execução
* @returns Mensagem formatada para o sistema
*/
export function formatToolResultForContext(
toolName: string,
result: any
): string {
const resultJson =
typeof result === "string" ? result : JSON.stringify(result, null, 2);
return `[TOOL RESULT - ${toolName}]
${resultJson}
[END TOOL RESULT]
Use as informações acima para responder ao usuário de forma natural. NÃO mencione que usou uma ferramenta.`;
}
+210
View File
@@ -0,0 +1,210 @@
/**
* Serviço para detectar intenções do usuário através de palavras-chave
*/
import type { DetectedIntent } from "@/lib/types/mcp-chat";
/**
* Padrões de keywords para cada tipo de intent
*/
const INTENT_PATTERNS = {
web_search: {
keywords: [
"buscar",
"pesquisar",
"procurar",
"google",
"encontrar",
"search",
"look up",
"find",
"o que é",
"quem é",
"onde fica",
],
regex:
/\b(buscar|pesquisar|procurar|google|encontrar|search|look up|find)\b/i,
},
time: {
keywords: [
"que horas",
"data",
"horário",
"hoje",
"agora",
"quando",
"calendário",
"time",
"date",
"current time",
"what time",
],
regex:
/\b(que horas|data|horário|hoje|agora|quando|time|date|current time)\b/i,
},
weather: {
keywords: [
"clima",
"tempo",
"temperatura",
"chuva",
"previsão",
"weather",
"forecast",
"temperature",
],
regex:
/\b(clima|tempo|temperatura|chuva|previsão|weather|forecast|temperature)\b/i,
},
file: {
keywords: [
"arquivo",
"pasta",
"diretório",
"file",
"folder",
"directory",
"ler arquivo",
"read file",
"listar",
"list",
],
regex:
/\b(arquivo|pasta|diretório|file|folder|directory|ler arquivo|read file)\b/i,
},
code: {
keywords: [
"código",
"programar",
"executar",
"run",
"execute",
"compile",
"debug",
"git",
"commit",
],
regex:
/\b(código|programar|executar|run|execute|compile|debug|git|commit)\b/i,
},
database: {
keywords: [
"banco de dados",
"database",
"query",
"sql",
"select",
"insert",
"update",
"delete",
],
regex:
/\b(banco de dados|database|query|sql|select|insert|update|delete)\b/i,
},
};
/**
* Detecta a intenção do usuário na mensagem
* @param message Mensagem do usuário
* @returns Intent detectado ou null
*/
export function detectIntent(message: string): DetectedIntent | null {
const messageLower = message.toLowerCase();
for (const [type, pattern] of Object.entries(INTENT_PATTERNS)) {
const matches = pattern.regex.test(messageLower);
if (matches) {
// Calcular confiança baseado em quantas keywords foram encontradas
const foundKeywords = pattern.keywords.filter((keyword) =>
messageLower.includes(keyword.toLowerCase())
);
const confidence = Math.min(0.5 + foundKeywords.length * 0.2, 1.0);
return {
type: type as DetectedIntent["type"],
confidence,
keywords: foundKeywords,
suggestedTools: getSuggestedToolsForIntent(
type as DetectedIntent["type"]
),
};
}
}
return null;
}
/**
* Retorna tools sugeridos para um tipo de intent
* @param intentType Tipo de intent
* @returns Array de nomes de tools sugeridos
*/
function getSuggestedToolsForIntent(
intentType: DetectedIntent["type"]
): string[] {
const toolMapping: Record<DetectedIntent["type"], string[]> = {
web_search: ["web_search", "duckduckgo_search", "google_search", "search"],
time: ["get_current_time", "get_date", "current_time", "time"],
weather: ["get_weather", "weather", "forecast"],
file: ["read_file", "list_files", "filesystem", "file"],
code: ["execute_code", "git", "run_command"],
database: ["query", "sql", "database"],
other: [],
};
return toolMapping[intentType] || [];
}
/**
* Verifica se um tool name corresponde a algum intent detectado
* @param toolName Nome do tool
* @param intent Intent detectado
* @returns true se o tool é relevante para o intent
*/
export function isToolRelevantForIntent(
toolName: string,
intent: DetectedIntent
): boolean {
const toolNameLower = toolName.toLowerCase();
return intent.suggestedTools.some((suggested) =>
toolNameLower.includes(suggested.toLowerCase())
);
}
/**
* Filtra tools com base em um intent detectado
* @param tools Array de tools disponíveis
* @param intent Intent detectado
* @returns Tools filtrados e ordenados por relevância
*/
export function filterToolsByIntent(
tools: Array<{ name: string; description: string }>,
intent: DetectedIntent
): Array<{ name: string; description: string; relevance: number }> {
return tools
.map((tool) => {
let relevance = 0;
// Verificar se o tool name contém alguma keyword sugerida
const toolNameLower = tool.name.toLowerCase();
for (const suggested of intent.suggestedTools) {
if (toolNameLower.includes(suggested.toLowerCase())) {
relevance += 2;
}
}
// Verificar se a descrição contém keywords do intent
const descriptionLower = tool.description.toLowerCase();
for (const keyword of intent.keywords) {
if (descriptionLower.includes(keyword.toLowerCase())) {
relevance += 1;
}
}
return { ...tool, relevance };
})
.filter((tool) => tool.relevance > 0)
.sort((a, b) => b.relevance - a.relevance);
}
+299
View File
@@ -0,0 +1,299 @@
/**
* Serviço executor de MCPs - spawna processos e executa tools via JSON-RPC
*/
import { spawn, type ChildProcess } from "child_process";
import { MCPRepository } from "@/database/repositories/mcp";
import type { MCPExecutionResult, MCPTool } from "@/lib/types/mcp-chat";
const EXECUTION_TIMEOUT = 30000; // 30 segundos
export class MCPExecutor {
/**
* Executa um tool de MCP
* @param mcpId ID do MCP instalado
* @param toolName Nome do tool a executar
* @param parameters Parâmetros do tool
* @returns Resultado da execução
*/
static async executeMCPTool(
mcpId: string,
toolName: string,
parameters: Record<string, any>
): Promise<MCPExecutionResult> {
const startTime = Date.now();
try {
// Obter dados do MCP instalado
const mcpData = MCPRepository.getInstalledMCP(mcpId);
if (!mcpData) {
throw new Error(`MCP ${mcpId} not found or not installed`);
}
// Verificar se está pronto
if (mcpData.status !== "ready") {
throw new Error(
`MCP ${mcpId} is not ready (status: ${mcpData.status})`
);
}
// Obter comando executável
const config = JSON.parse(mcpData.config);
const command = config.command || mcpData.executable_command;
const args = config.args || [];
if (!command) {
throw new Error(`No executable command found for MCP ${mcpId}`);
}
console.log(`🚀 Executing MCP tool: ${mcpId}.${toolName}`);
console.log(`Command: ${command} ${args.join(" ")}`);
// Spawnar processo do MCP
const result = await this.spawnMCPProcess(
command,
args,
toolName,
parameters,
mcpData.environment_path
);
const executionTime = Date.now() - startTime;
return {
success: true,
toolName,
mcpId,
result,
executionTime,
};
} catch (error: any) {
const executionTime = Date.now() - startTime;
console.error(`❌ Error executing MCP tool ${mcpId}.${toolName}:`, error);
return {
success: false,
toolName,
mcpId,
error: error.message || "Unknown error",
executionTime,
};
}
}
/**
* Spawna processo do MCP e executa JSON-RPC
* @param command Comando executável
* @param args Argumentos do comando
* @param toolName Nome do tool
* @param parameters Parâmetros do tool
* @param environmentPath Caminho do ambiente (opcional)
* @returns Resultado do tool
*/
private static async spawnMCPProcess(
command: string,
args: string[],
toolName: string,
parameters: Record<string, any>,
environmentPath?: string
): Promise<any> {
return new Promise((resolve, reject) => {
const env = {
...process.env,
...(environmentPath && {
PATH: `${environmentPath}/node_modules/.bin:${process.env.PATH}`,
}),
};
const process = spawn(command, args, {
cwd: environmentPath || process.cwd(),
env,
stdio: ["pipe", "pipe", "pipe"],
});
let stdout = "";
let stderr = "";
let responseReceived = false;
// Timeout
const timeout = setTimeout(() => {
if (!responseReceived) {
process.kill();
reject(new Error(`Execution timeout after ${EXECUTION_TIMEOUT}ms`));
}
}, EXECUTION_TIMEOUT);
// Capturar stdout
process.stdout?.on("data", (data) => {
stdout += data.toString();
// Tentar parsear respostas JSON-RPC
const lines = stdout.split("\n");
for (const line of lines) {
if (line.trim()) {
try {
const parsed = JSON.parse(line);
if (parsed.id === 2 && parsed.result) {
// Resposta do tools/call
clearTimeout(timeout);
responseReceived = true;
process.kill();
resolve(parsed.result);
}
} catch (e) {
// Linha não é JSON válido, continuar
}
}
}
});
// Capturar stderr
process.stderr?.on("data", (data) => {
stderr += data.toString();
});
// Erro de processo
process.on("error", (error) => {
clearTimeout(timeout);
reject(new Error(`Process error: ${error.message}`));
});
// Processo encerrou
process.on("close", (code) => {
clearTimeout(timeout);
if (!responseReceived) {
if (code !== 0) {
reject(
new Error(`Process exited with code ${code}. stderr: ${stderr}`)
);
} else {
reject(new Error("No valid response received from MCP"));
}
}
});
// Enviar requisições JSON-RPC
try {
// 1. Initialize
const initRequest = {
jsonrpc: "2.0",
method: "initialize",
params: {
protocolVersion: "2024-11-05",
capabilities: {},
clientInfo: { name: "ollahub", version: "1.0.0" },
},
id: 1,
};
process.stdin?.write(JSON.stringify(initRequest) + "\n");
// Aguardar um pouco antes de enviar tool call
setTimeout(() => {
// 2. Call tool
const toolCallRequest = {
jsonrpc: "2.0",
method: "tools/call",
params: {
name: toolName,
arguments: parameters,
},
id: 2,
};
process.stdin?.write(JSON.stringify(toolCallRequest) + "\n");
process.stdin?.end();
}, 500);
} catch (error: any) {
clearTimeout(timeout);
process.kill();
reject(new Error(`Failed to send JSON-RPC request: ${error.message}`));
}
});
}
/**
* Obter tools disponíveis de um MCP
* @param mcpId ID do MCP
* @returns Array de tools
*/
static getAvailableTools(mcpId: string): MCPTool[] {
try {
const mcpData = MCPRepository.getInstalledMCP(mcpId);
if (!mcpData || !mcpData.tools) {
return [];
}
// Se já é um array (já foi parseado), retornar diretamente
if (Array.isArray(mcpData.tools)) {
return mcpData.tools;
}
// Se é string, parsear
if (typeof mcpData.tools === "string") {
const tools = JSON.parse(mcpData.tools);
return Array.isArray(tools) ? tools : [];
}
return [];
} catch (error) {
console.error(`Error getting available tools for ${mcpId}:`, error);
return [];
}
}
/**
* Validar parâmetros contra schema do tool
* @param parameters Parâmetros fornecidos
* @param tool Definição do tool
* @returns true se válido, false caso contrário
*/
static validateParameters(
parameters: Record<string, any>,
tool: MCPTool
): { valid: boolean; error?: string } {
try {
const schema = tool.inputSchema;
// Verificar campos obrigatórios
if (schema.required) {
for (const required of schema.required) {
if (!(required in parameters)) {
return {
valid: false,
error: `Missing required parameter: ${required}`,
};
}
}
}
// Validação básica de tipos
for (const [key, value] of Object.entries(parameters)) {
if (schema.properties && schema.properties[key]) {
const propSchema = schema.properties[key];
const expectedType = propSchema.type;
if (expectedType) {
const actualType = typeof value;
const valid =
(expectedType === "string" && actualType === "string") ||
(expectedType === "number" && actualType === "number") ||
(expectedType === "boolean" && actualType === "boolean") ||
(expectedType === "object" && actualType === "object") ||
(expectedType === "array" && Array.isArray(value));
if (!valid) {
return {
valid: false,
error: `Parameter ${key} should be ${expectedType}, got ${actualType}`,
};
}
}
}
}
return { valid: true };
} catch (error: any) {
return { valid: false, error: error.message };
}
}
}
+77
View File
@@ -0,0 +1,77 @@
/**
* Serviço para detectar capabilities de function calling dos modelos
*/
import type { ModelCapabilities } from "@/lib/types/mcp-chat";
/**
* Lista de modelos com suporte nativo a function calling
* Baseado na documentação do Ollama (atualizado em nov 2024)
*/
const MODELS_WITH_NATIVE_TOOLS = [
// Llama 3.x
/^llama3\.1/i,
/^llama3\.2/i,
/^llama3-/i,
// Mistral
/^mistral/i,
/^mistral-nemo/i,
/^mixtral/i,
// Gemma
/^gemma2/i,
// Qwen
/^qwen2\.5/i,
// Command-R
/^command-r/i,
// Firefunction
/^firefunction/i,
];
/**
* Detecta capabilities de function calling de um modelo
* @param modelName Nome do modelo (ex: "llama3.1:8b", "qwen2.5:7b")
* @returns Capabilities do modelo
*/
export function detectModelCapabilities(modelName: string): ModelCapabilities {
const supportsNativeTools = MODELS_WITH_NATIVE_TOOLS.some((pattern) =>
pattern.test(modelName)
);
return {
modelName,
supportsNativeTools,
requiresPromptEngineering: !supportsNativeTools,
toolCallFormat: supportsNativeTools ? "openai" : undefined,
};
}
/**
* Verifica se um modelo específico suporta function calling nativo
* @param modelName Nome do modelo
* @returns true se suporta, false caso contrário
*/
export function supportsNativeTools(modelName: string): boolean {
return detectModelCapabilities(modelName).supportsNativeTools;
}
/**
* Lista todos os modelos conhecidos com suporte a function calling
* @returns Array de padrões regex em string
*/
export function getSupportedModelsPatterns(): string[] {
return [
"llama3.1:*",
"llama3.2:*",
"mistral:*",
"mixtral:*",
"gemma2:*",
"qwen2.5:*",
"command-r:*",
"firefunction:*",
];
}
+74
View File
@@ -0,0 +1,74 @@
/**
* Tipos e interfaces para integração de MCPs no chat
*/
/**
* Representa um tool MCP com schema JSON
*/
export interface MCPTool {
name: string;
description: string;
inputSchema: {
type: "object";
properties: Record<string, any>;
required?: string[];
};
}
/**
* Contexto de MCPs ativos para a conversa
*/
export interface MCPChatContext {
mcpId: string;
enabled: boolean;
tools: MCPTool[];
config: any;
}
/**
* Resultado da execução de um MCP
*/
export interface MCPExecutionResult {
success: boolean;
toolName: string;
mcpId: string;
result?: any;
error?: string;
executionTime?: number;
}
/**
* Capabilities de function calling do modelo
*/
export interface ModelCapabilities {
supportsNativeTools: boolean;
requiresPromptEngineering: boolean;
modelName: string;
toolCallFormat?: "openai" | "anthropic" | "custom";
}
/**
* Configuração de MCPs para chat
*/
export interface MCPChatConfig {
alwaysActive: string[]; // MCPs sempre ativos
neverUse: string[]; // MCPs nunca usar
activationMode: "automatic" | "keywords" | "manual";
}
/**
* Intent detectado na mensagem do usuário
*/
export interface DetectedIntent {
type:
| "web_search"
| "time"
| "weather"
| "file"
| "code"
| "database"
| "other";
confidence: number;
keywords: string[];
suggestedTools: string[];
}