chore: remove deprecated files and configurations

- Deleted .gitignore, biome.json, next.config.ts, package.json, pnpm-lock.yaml, pnpm-workspace.yaml, postcss.config.mjs, README.md, and various API, component, and utility files to clean up the codebase.
- Removed unused database files and migration scripts to streamline the project structure.
- Eliminated obsolete hooks and services to enhance maintainability and reduce clutter.
This commit is contained in:
Evandro Fonseca Junior committed 2025-11-18 21:43:11 -03:00
1 parent a8bb7cc22d
commit ad9d5d32d2
119 files changed
-17633

No files matched your search

-173
View File
@@ -1,173 +0,0 @@
import { DeepNLPSearchParams } from "@/lib/types/mcp";
interface CacheEntry {
data: any;
timestamp: number;
ttl: number;
}
export class MCPCache {
private static cache = new Map<string, CacheEntry>();
private static TTL = 5 * 60 * 1000; // 5 minutos em millisegundos
/**
* Obter dados do cache
*/
static get(key: string): any | null {
const entry = this.cache.get(key);
if (!entry) {
return null;
}
// Verificar se expirou
const now = Date.now();
if (now - entry.timestamp > entry.ttl) {
this.cache.delete(key);
return null;
}
console.log(`Cache hit for key: ${key}`);
return entry.data;
}
/**
* Salvar dados no cache
*/
static set(key: string, value: any, ttl?: number): void {
const entry: CacheEntry = {
data: value,
timestamp: Date.now(),
ttl: ttl || this.TTL
};
this.cache.set(key, entry);
console.log(`Cache set for key: ${key}, TTL: ${entry.ttl}ms`);
}
/**
* Limpar cache
*/
static clear(): void {
this.cache.clear();
console.log("Cache cleared");
}
/**
* Remover entrada específica do cache
*/
static delete(key: string): void {
this.cache.delete(key);
console.log(`Cache entry deleted for key: ${key}`);
}
/**
* Gerar chave de cache baseada nos parâmetros de busca
*/
static generateKey(params: DeepNLPSearchParams): string {
const sortedParams = Object.keys(params)
.sort()
.reduce((result, key) => {
const value = params[key as keyof DeepNLPSearchParams];
if (value !== undefined && value !== null) {
result[key] = value;
}
return result;
}, {} as Record<string, any>);
return `mcp_search_${JSON.stringify(sortedParams)}`;
}
/**
* Gerar chave para configuração de servidor
*/
static generateServerConfigKey(mcpId: string): string {
return `mcp_config_${mcpId}`;
}
/**
* Gerar chave para ferramentas de servidor
*/
static generateServerToolsKey(mcpId: string): string {
return `mcp_tools_${mcpId}`;
}
/**
* Obter estatísticas do cache
*/
static getStats(): {
size: number;
entries: Array<{ key: string; age: number; ttl: number }>;
} {
const now = Date.now();
const entries = Array.from(this.cache.entries()).map(([key, entry]) => ({
key,
age: now - entry.timestamp,
ttl: entry.ttl
}));
return {
size: this.cache.size,
entries
};
}
/**
* Limpar entradas expiradas
*/
static cleanup(): number {
const now = Date.now();
let cleaned = 0;
for (const [key, entry] of this.cache.entries()) {
if (now - entry.timestamp > entry.ttl) {
this.cache.delete(key);
cleaned++;
}
}
if (cleaned > 0) {
console.log(`Cache cleanup: removed ${cleaned} expired entries`);
}
return cleaned;
}
/**
* Verificar se uma chave existe no cache (sem verificar TTL)
*/
static has(key: string): boolean {
return this.cache.has(key);
}
/**
* Obter todas as chaves do cache
*/
static keys(): string[] {
return Array.from(this.cache.keys());
}
/**
* Definir TTL personalizado para diferentes tipos de dados
*/
static getTTLForDataType(dataType: 'search' | 'config' | 'tools'): number {
switch (dataType) {
case 'search':
return 5 * 60 * 1000; // 5 minutos para buscas
case 'config':
return 30 * 60 * 1000; // 30 minutos para configurações
case 'tools':
return 15 * 60 * 1000; // 15 minutos para ferramentas
default:
return this.TTL;
}
}
/**
* Cache com TTL específico para tipo de dados
*/
static setWithDataType(key: string, value: any, dataType: 'search' | 'config' | 'tools'): void {
const ttl = this.getTTLForDataType(dataType);
this.set(key, value, ttl);
}
}
-50
View File
@@ -1,50 +0,0 @@
import fs from "node:fs/promises";
import path from "node:path";
type CatalogModel = {
name: string;
description?: string;
capabilities?: string[];
parameter_sizes?: string[];
pulls?: string;
tags_count?: number;
last_updated?: string;
url?: string;
};
type CatalogFile = {
models?: CatalogModel[];
};
let cachedModels: CatalogModel[] | null = null;
let cachedMtimeMs: number | null = null;
function resolveCatalogPath(): string {
// Arquivo fica em data/models.json na raiz do projeto
return path.join(process.cwd(), "data", "models.json");
}
export async function getCatalogModels(): Promise<CatalogModel[]> {
try {
const catalogPath = resolveCatalogPath();
const stat = await fs.stat(catalogPath);
// Cache simples por mtime
if (cachedModels && cachedMtimeMs && stat.mtimeMs === cachedMtimeMs) {
return cachedModels;
}
const raw = await fs.readFile(catalogPath, "utf-8");
const json = JSON.parse(raw) as CatalogFile;
const models = Array.isArray(json.models) ? json.models : [];
cachedModels = models;
cachedMtimeMs = stat.mtimeMs;
return models;
} catch (error) {
// Em caso de erro, retorne array vazio para não quebrar a UI
return [];
}
}
-50
View File
@@ -1,50 +0,0 @@
export type ChatRole = "user" | "assistant" | "system";
export type Message = {
id: string;
role: ChatRole;
content: string;
timestamp: number;
};
export type ChatState = {
messages: Message[];
selectedModel: string | null;
isStreaming: boolean;
streamingContent: string;
};
export function createMessage(role: ChatRole, content: string): Message {
return {
id: crypto.randomUUID(),
role,
content,
timestamp: Date.now(),
};
}
export function formatTimestamp(timestamp: number): string {
const now = Date.now();
const diff = now - timestamp;
const minutes = Math.floor(diff / 60000);
if (minutes < 1) return "agora";
if (minutes < 60) return `${minutes} min atrás`;
const hours = Math.floor(minutes / 60);
if (hours < 24) return `${hours}h atrás`;
const days = Math.floor(hours / 24);
return `${days}d atrás`;
}
export function convertToOllamaMessages(messages: Message[]) {
return messages.map((msg) => ({
role: msg.role,
content: msg.content,
}));
}
export function createSystemMessage(content: string): Message {
return createMessage("system", content);
}
-33
View File
@@ -1,33 +0,0 @@
/**
* Configurações para instalação e gerenciamento de servidores MCP
*/
import path from "path";
export const MCP_CONFIG = {
// Diretório base onde os servidores MCP serão instalados
serversDir:
process.env.MCP_SERVERS_DIR || path.join(process.cwd(), "mcp-servers"),
// Timeout para instalação de pacotes (5 minutos)
installTimeout: parseInt(process.env.MCP_INSTALL_TIMEOUT || "300000", 10),
// Timeout para testes de servidor (10 segundos)
testTimeout: parseInt(process.env.MCP_TEST_TIMEOUT || "10000", 10),
// Habilitar logs detalhados (para debug/testes)
enableLogs: process.env.MCP_ENABLE_LOGS === "true",
// Diretórios por tipo de pacote
get npmDir() {
return path.join(this.serversDir, "npm");
},
get pythonDir() {
return path.join(this.serversDir, "python");
},
get rustDir() {
return path.join(this.serversDir, "rust");
},
};
-131
View File
@@ -1,131 +0,0 @@
import { Ollama } from "ollama";
const ollama = new Ollama({
host: process.env.OLLAMA_HOST || "http://localhost:11434",
});
export async function generateEmbedding(
text: string,
model: string = "nomic-embed-text"
): Promise<Float32Array> {
try {
// Verificar se deve forçar uso de CPU (útil quando GPU tem problemas de memória)
const forceCpu =
process.env.OLLAMA_NO_GPU === "1" || process.env.OLLAMA_NUM_GPU === "0";
const response = await ollama.embeddings({
model,
prompt: text,
options: forceCpu ? { num_gpu: 0 } : undefined,
});
return new Float32Array(response.embedding);
} catch (error: any) {
console.error("Erro ao gerar embedding:", error);
// Se erro de GPU e ainda não tentou CPU, tentar forçar CPU
const isGpuError =
error?.message?.includes("CUDA") ||
error?.message?.includes("unable to allocate") ||
error?.status_code === 500;
if (
isGpuError &&
process.env.OLLAMA_NO_GPU !== "1" &&
process.env.OLLAMA_NUM_GPU !== "0"
) {
console.warn("Erro de GPU detectado, tentando usar CPU...");
try {
const response = await ollama.embeddings({
model,
prompt: text,
options: { num_gpu: 0 },
});
return new Float32Array(response.embedding);
} catch (cpuError) {
console.error("Erro mesmo usando CPU:", cpuError);
}
}
// Fallback: retornar vetor zero se modelo não estiver disponível
console.warn(
`Modelo ${model} não disponível, usando vetor zero como fallback`
);
return new Float32Array(768); // 768 dimensões para nomic-embed-text
}
}
const EMOJI_CATEGORIES = {
code: ["💻", "🖥️", "⌨️", "🔧", "🛠️"],
idea: ["💡", "🧠", "💭", "🤔", "✨"],
question: ["❓", "🤷", "🔍", "🧐", "📚"],
creative: ["🎨", "🎭", "🎪", "🌈", "🎬"],
business: ["💼", "📊", "💰", "📈", "🏢"],
science: ["🔬", "🧪", "🔭", "⚗️", "🧬"],
education: ["📖", "📝", "🎓", "✏️", "📚"],
communication: ["💬", "📱", "📧", "🗣️", "💌"],
general: ["🌟", "🎯", "🚀", "⚡", "🔥"],
};
export async function generateChatTitle(firstMessage: string): Promise<string> {
try {
const response = await ollama.generate({
model: "qwen2.5:0.5b",
prompt: `Analise esta mensagem e crie um título curto e descritivo (MAX 30 caracteres, sem emoji):
Mensagem: "${firstMessage}"
Título (direto, sem explicação):`,
options: {
temperature: 0.7,
num_predict: 15,
},
});
let title = response.response
.trim()
.replace(/['"]/g, "")
.replace(/^Título:\s*/i, "")
.substring(0, 30);
// Selecionar emoji baseado no conteúdo
const emoji = selectEmoji(firstMessage);
return `${emoji} ${title}`;
} catch (error) {
console.error("Erro ao gerar título:", error);
// Fallback: usar título simples baseado no conteúdo
const emoji = selectEmoji(firstMessage);
const fallbackTitle = firstMessage.substring(0, 20).trim();
return `${emoji} ${fallbackTitle}`;
}
}
function selectEmoji(content: string): string {
const lower = content.toLowerCase();
if (/code|program|debug|function|class/.test(lower))
return EMOJI_CATEGORIES.code[
Math.floor(Math.random() * EMOJI_CATEGORIES.code.length)
];
if (/why|how|what|when|where/.test(lower))
return EMOJI_CATEGORIES.question[
Math.floor(Math.random() * EMOJI_CATEGORIES.question.length)
];
if (/idea|think|create|design/.test(lower))
return EMOJI_CATEGORIES.idea[
Math.floor(Math.random() * EMOJI_CATEGORIES.idea.length)
];
if (/business|money|sale|market/.test(lower))
return EMOJI_CATEGORIES.business[
Math.floor(Math.random() * EMOJI_CATEGORIES.business.length)
];
if (/science|research|experiment/.test(lower))
return EMOJI_CATEGORIES.science[
Math.floor(Math.random() * EMOJI_CATEGORIES.science.length)
];
return EMOJI_CATEGORIES.general[
Math.floor(Math.random() * EMOJI_CATEGORIES.general.length)
];
}
View File
Whitespace-only changes.
-100
View File
@@ -1,100 +0,0 @@
export type Device = "CPU" | "GPU";
export type ModelInfo = {
id: string;
name: string;
sizeGB: number;
quantization: string;
device: Device;
estCpuUsagePct?: number;
estVramGB?: number;
};
export const mockModels: ModelInfo[] = [
{
id: "llama3.1-8b-q4",
name: "llama3.1:8b-q4",
sizeGB: 4.8,
quantization: "Q4",
device: "CPU",
estCpuUsagePct: 85,
estVramGB: 0.0,
},
{
id: "llama3.1-8b-q8",
name: "llama3.1:8b-q8",
sizeGB: 8.9,
quantization: "Q8",
device: "GPU",
estCpuUsagePct: 20,
estVramGB: 8,
},
{
id: "mistral-7b-q4",
name: "mistral:7b-q4",
sizeGB: 4.1,
quantization: "Q4",
device: "CPU",
estCpuUsagePct: 75,
estVramGB: 0,
},
{
id: "mistral-7b-q8",
name: "mistral:7b-q8",
sizeGB: 7.9,
quantization: "Q8",
device: "GPU",
estCpuUsagePct: 18,
estVramGB: 8,
},
{
id: "qwen2.5-7b-q4",
name: "qwen2.5:7b-q4",
sizeGB: 4.2,
quantization: "Q4",
device: "CPU",
estCpuUsagePct: 80,
estVramGB: 0,
},
{
id: "qwen2.5-7b-q8",
name: "qwen2.5:7b-q8",
sizeGB: 8.1,
quantization: "Q8",
device: "GPU",
estCpuUsagePct: 22,
estVramGB: 8,
},
];
export function inferQuantization(nameOrDetails: string | unknown): string {
const s =
typeof nameOrDetails === "string" ? nameOrDetails.toLowerCase() : "";
if (s.includes("q8")) return "Q8";
if (s.includes("q5")) return "Q5";
if (s.includes("q4")) return "Q4";
return "Q?";
}
export function inferDevice(details?: unknown): Device {
const s = typeof details === "string" ? details.toLowerCase() : "";
if (s.includes("cuda") || s.includes("metal") || s.includes("gpu"))
return "GPU";
return "CPU";
}
export function estimateCpuVram(
info: Pick<ModelInfo, "sizeGB" | "quantization" | "device">,
) {
const q = info.quantization.toUpperCase();
let cpuPct = 50;
let vramGB = 0;
if (info.device === "GPU") {
vramGB = Math.max(4, Math.round(info.sizeGB));
cpuPct = 20;
} else {
// heurística simples: quantizações mais baixas usam mais CPU
cpuPct = q === "Q8" ? 60 : q === "Q5" ? 70 : 85;
}
return { cpuPct, vramGB };
}
-200
View File
@@ -1,200 +0,0 @@
import { Ollama } from "ollama";
import os from "node:os";
import { spawn } from "node:child_process";
import { getCatalogModels } from "./catalog";
export const OLLAMA_HOST = process.env.OLLAMA_HOST || "http://localhost:11434";
export async function listModelsViaSdk() {
try {
const client = new Ollama({ host: OLLAMA_HOST });
const res = await client.list();
return res?.models ?? [];
} catch (e) {
throw e;
}
}
export async function listModelsViaHttp() {
const url = `${OLLAMA_HOST}/api/tags`;
const res = await fetch(url, { cache: "no-store" });
if (!res.ok) throw new Error(`Ollama HTTP error ${res.status}`);
const data = await res.json();
return data?.models ?? [];
}
async function isOllamaHealthy(): Promise<boolean> {
try {
await listModelsViaHttp();
return true;
} catch {
return false;
}
}
async function waitUntilHealthy(timeoutMs: number, intervalMs = 800): Promise<boolean> {
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
if (await isOllamaHealthy()) return true;
await new Promise((r) => setTimeout(r, intervalMs));
}
return false;
}
export async function ensureOllamaAvailable(options?: { timeoutMs?: number }): Promise<boolean> {
const timeoutMs = options?.timeoutMs ?? 20000;
if (await isOllamaHealthy()) return true;
// Windows: tentar iniciar via PowerShell script (WSL-first fallback Windows)
if (os.platform() === "win32") {
try {
const child = spawn(
"powershell",
[
"-ExecutionPolicy",
"Bypass",
"-File",
"scripts/ensure-ollama.ps1",
],
{ stdio: "ignore", detached: true }
);
child.unref();
} catch (e) {
// Ignorar, tentaremos apenas aguardar
}
}
// Aguardar saúde
return await waitUntilHealthy(timeoutMs);
}
export async function chatWithStream(
model: string,
messages: Array<{ role: string; content: string }>,
options?: Record<string, unknown>,
system?: string,
tools?: Array<any>
) {
const client = new Ollama({ host: OLLAMA_HOST });
const chatOptions: any = {
model,
messages: system
? [{ role: "system", content: system }, ...messages]
: messages,
stream: true,
options: { ...(options || {}), keep_alive: "5m" },
};
// Adicionar tools se fornecidos
if (tools && tools.length > 0) {
chatOptions.tools = tools;
}
return client.chat(chatOptions);
}
export async function pullModel(model: string) {
const client = new Ollama({ host: OLLAMA_HOST });
return client.pull({ model, stream: true });
}
export async function checkModelExists(modelName: string): Promise<boolean> {
try {
const models = await listModelsViaSdk();
return models.some((model) => model.name === modelName);
} catch (error) {
console.error("Erro ao verificar modelos:", error);
return false;
}
}
export async function ensureModelExists(
modelName: string,
description?: string
): Promise<void> {
const exists = await checkModelExists(modelName);
if (!exists) {
console.log(`📥 Baixando modelo ${modelName}...`);
if (description) {
console.log(`ℹ️ ${description}`);
}
const stream = await pullModel(modelName);
let downloadedBytes = 0;
let totalBytes = 0;
let progress = 0;
// Processar stream para mostrar progresso
for await (const chunk of stream) {
if (chunk.status === "downloading" && chunk.completed && chunk.total) {
downloadedBytes = chunk.completed;
totalBytes = chunk.total;
progress = Math.round((downloadedBytes / totalBytes) * 100);
// Criar barra de progresso visual
const barLength = 20;
const filledLength = Math.round((progress / 100) * barLength);
const bar =
"█".repeat(filledLength) + "░".repeat(barLength - filledLength);
// Mostrar progresso com informações
const downloadedMB = (downloadedBytes / 1024 / 1024).toFixed(1);
const totalMB = (totalBytes / 1024 / 1024).toFixed(1);
process.stdout.write(
`\r📥 ${modelName}: [${bar}] ${progress}% (${downloadedMB}MB/${totalMB}MB)`
);
} else if (chunk.status === "success") {
console.log(`\n✅ Modelo ${modelName} baixado com sucesso!`);
if (description) {
console.log(
`🎯 ${description.replace("será usado", "agora funciona")}`
);
}
break;
}
}
} else {
console.log(`✅ Modelo ${modelName} já está disponível`);
}
}
export async function deleteModel(modelName: string): Promise<void> {
const client = new Ollama({ host: OLLAMA_HOST });
await client.delete({ model: modelName });
}
export async function searchRemoteModels(query?: string): Promise<any[]> {
try {
const catalog = await getCatalogModels();
// Normalizar pesquisa
const q = (query || "").trim().toLowerCase();
const filtered = q
? catalog.filter((m) => {
const name = m.name?.toLowerCase() || "";
const desc = m.description?.toLowerCase() || "";
return name.includes(q) || desc.includes(q);
})
: catalog;
// Modelos instalados
const installedModels = await listModelsViaSdk();
const installedNames = new Set(installedModels.map((m) => m.name));
// Retorno no formato esperado pelo frontend
return filtered.map((m) => ({
name: m.name,
description: m.description,
url: m.url,
tags_count: m.tags_count,
installed: installedNames.has(m.name),
remote: true,
}));
} catch (error) {
console.error("Erro ao buscar modelos remotos:", error);
return [];
}
}
-99
View File
@@ -1,99 +0,0 @@
import Database from "better-sqlite3";
import path from "path";
import fs from "fs";
let db: Database.Database | null = null;
export async function getPreferencesDb(): Promise<Database.Database> {
if (!db) {
try {
const dbPath = path.join(
process.cwd(),
"database",
"ollahub-preferences.db"
);
// Garantir que o diretório existe
const dbDir = path.dirname(dbPath);
if (!fs.existsSync(dbDir)) {
fs.mkdirSync(dbDir, { recursive: true });
}
db = new Database(dbPath);
// Configurações de performance
db.pragma("journal_mode = WAL");
db.pragma("synchronous = NORMAL");
db.pragma("foreign_keys = ON");
db.pragma("cache_size = 10000");
db.pragma("temp_store = memory");
// Criar tabela de preferências se não existir
db.exec(`
CREATE TABLE IF NOT EXISTS user_preferences (
user_id TEXT PRIMARY KEY,
preferences TEXT NOT NULL,
updated_at INTEGER NOT NULL
);
`);
console.log("✅ Banco de preferências SQLite conectado com sucesso");
} catch (error) {
console.warn("⚠️ Erro ao conectar banco de preferências:", error);
throw error;
}
}
return db;
}
export async function getPreferences(userId: string = "default"): Promise<any> {
try {
const database = await getPreferencesDb();
const stmt = database.prepare(
"SELECT preferences FROM user_preferences WHERE user_id = ?"
);
const result = stmt.get(userId) as { preferences: string } | undefined;
if (result) {
return JSON.parse(result.preferences);
}
// Retornar preferências padrão
return {
selectedModel: null,
systemPrompt: "",
device: "auto",
gpuIndex: 0,
numGpu: 1,
};
} catch (error) {
console.error("Erro ao buscar preferências:", error);
throw error;
}
}
export async function setPreferences(
userId: string = "default",
preferences: any
): Promise<void> {
try {
const database = await getPreferencesDb();
const stmt = database.prepare(`
INSERT OR REPLACE INTO user_preferences (user_id, preferences, updated_at)
VALUES (?, ?, ?)
`);
stmt.run(userId, JSON.stringify(preferences), Date.now());
console.log("✅ Preferências salvas no SQLite");
} catch (error) {
console.error("Erro ao salvar preferências:", error);
throw error;
}
}
export function closePreferencesDb(): void {
if (db) {
db.close();
db = null;
}
}
-242
View File
@@ -1,242 +0,0 @@
/**
* 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());
// Formato atual suportado pelo prompt engineering
if (parsed.tool_call && parsed.tool_call.name) {
return {
name: parsed.tool_call.name,
parameters: parsed.tool_call.parameters || {},
};
}
// Formato OpenAI-like: { type: 'function', function: { name, parameters|arguments } }
// Algumas implementações usam `arguments` como string JSON
if (parsed.type === "function" && parsed.function && parsed.function.name) {
const fn = parsed.function;
let params: any = fn.parameters ?? fn.arguments ?? {};
if (typeof params === "string") {
try {
params = JSON.parse(params);
} catch {
// se não for JSON válido, mantém string (executor fará fallback)
}
}
return { name: fn.name, parameters: params || {} };
}
// Formato simples: { name: "tool", parameters: {...} }
if (parsed.name && (parsed.parameters || parsed.arguments)) {
let params: any = parsed.parameters ?? parsed.arguments ?? {};
if (typeof params === "string") {
try {
params = JSON.parse(params);
} catch {
// mantém string se não for JSON válido
}
}
return { name: parsed.name, parameters: params || {} };
}
return null;
} catch (error) {
// Tentar extrair manualmente
const nameMatch = response.match(/"name":\s*"([^"]+)"/);
if (nameMatch) {
const name = nameMatch[1];
// Captura tanto parameters quanto arguments
const paramsMatch =
response.match(/"parameters"\s*:\s*({[\s\S]*?})/) ||
response.match(/"arguments"\s*:\s*("[\s\S]*?"|{[\s\S]*?})/);
let parameters: any = {};
if (paramsMatch) {
const raw = paramsMatch[1];
try {
parameters = JSON.parse(raw);
} catch {
// caso venha como string com JSON dentro de aspas
if (raw.startsWith('"') && raw.endsWith('"')) {
try {
parameters = JSON.parse(raw.slice(1, -1));
} catch {
parameters = {};
}
}
}
}
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.`;
}
-341
View File
@@ -1,341 +0,0 @@
import {
DeepNLPSearchParams,
DeepNLPResponse,
MCPServerConfig,
MCPTool,
MCPProvider,
} from "@/lib/types/mcp";
import { MCPCache } from "@/lib/cache/mcp-cache";
const DEEPNLP_API_BASE = "https://www.deepnlp.org/api/mcp_marketplace/v1";
export class DeepNLPService {
// Mapear categorias do projeto para categorias DeepNLP
private static categoryMap = {
productivity: ["office"],
development: ["code", "browser"],
data: ["database"],
integration: ["search", "payment"],
other: ["map", "chart", "finance"],
};
// Mapear categorias DeepNLP para categorias internas
private static reverseCategoryMap: Record<string, MCPProvider["category"]> = {
office: "office",
code: "code",
browser: "browser",
database: "database",
search: "search",
payment: "payment",
map: "map",
chart: "chart",
finance: "finance",
};
/**
* Buscar servidores MCP na API DeepNLP
*/
static async searchMCPs(
params: DeepNLPSearchParams,
retryCount = 0
): Promise<DeepNLPResponse> {
const maxRetries = 3;
const baseDelay = 1000; // 1 segundo
try {
// Verificar cache primeiro
const cacheKey = MCPCache.generateKey(params);
const cachedResult = MCPCache.get(cacheKey);
if (cachedResult) {
console.log("Returning cached search results");
return cachedResult;
}
const url = new URL(DEEPNLP_API_BASE);
// Adicionar parâmetros de busca
if (params.query) url.searchParams.set("query", params.query);
if (params.category) url.searchParams.set("category", params.category);
if (params.id) url.searchParams.set("id", params.id);
if (params.page_id !== undefined)
url.searchParams.set("page_id", params.page_id.toString());
if (params.count_per_page !== undefined)
url.searchParams.set(
"count_per_page",
params.count_per_page.toString()
);
if (params.offset !== undefined)
url.searchParams.set("offset", params.offset.toString());
if (params.mode) url.searchParams.set("mode", params.mode);
console.log(
`DeepNLP API Request (attempt ${retryCount + 1}):`,
url.toString()
);
const response = await fetch(url.toString(), {
method: "GET",
headers: {
Accept: "application/json",
"User-Agent": "Ollahub-MCP-Client/1.0",
},
// Timeout de 10 segundos
signal: AbortSignal.timeout(10000),
});
if (!response.ok) {
throw new Error(
`DeepNLP API error: ${response.status} ${response.statusText}`
);
}
const data = await response.json();
console.log("DeepNLP API Response:", data);
// Salvar no cache
MCPCache.setWithDataType(cacheKey, data, "search");
return data;
} catch (error) {
console.error(
`Error calling DeepNLP API (attempt ${retryCount + 1}):`,
error
);
// Se ainda temos tentativas restantes, tentar novamente com backoff exponencial
if (retryCount < maxRetries) {
const delay = baseDelay * Math.pow(2, retryCount); // Backoff exponencial
console.log(`Retrying in ${delay}ms...`);
await new Promise((resolve) => setTimeout(resolve, delay));
return this.searchMCPs(params, retryCount + 1);
}
// Se esgotamos as tentativas, lançar erro
throw new Error(
`Failed to fetch MCPs after ${maxRetries + 1} attempts: ${
error instanceof Error ? error.message : "Unknown error"
}`
);
}
}
/**
* Obter configuração completa de um servidor específico
*/
static async getServerConfig(
id: string,
retryCount = 0
): Promise<MCPServerConfig> {
const maxRetries = 3;
const baseDelay = 1000;
try {
// Verificar cache primeiro
const cacheKey = MCPCache.generateServerConfigKey(id);
const cachedResult = MCPCache.get(cacheKey);
if (cachedResult) {
console.log("Returning cached server config");
return cachedResult;
}
const [owner, repo] = id.split("/");
if (!owner || !repo) {
throw new Error(
`Invalid MCP ID format: ${id}. Expected format: owner/repo`
);
}
const url = `${DEEPNLP_API_BASE}/server/${owner}/${repo}`;
console.log(
`DeepNLP Server Config Request (attempt ${retryCount + 1}):`,
url
);
const response = await fetch(url, {
method: "GET",
headers: {
Accept: "application/json",
"User-Agent": "Ollahub-MCP-Client/1.0",
},
signal: AbortSignal.timeout(10000),
});
if (!response.ok) {
throw new Error(
`DeepNLP Server Config API error: ${response.status} ${response.statusText}`
);
}
const data = await response.json();
console.log("DeepNLP Server Config Response:", data);
// Salvar no cache
MCPCache.setWithDataType(cacheKey, data, "config");
return data;
} catch (error) {
console.error(
`Error getting server config from DeepNLP (attempt ${retryCount + 1}):`,
error
);
if (retryCount < maxRetries) {
const delay = baseDelay * Math.pow(2, retryCount);
console.log(`Retrying server config in ${delay}ms...`);
await new Promise((resolve) => setTimeout(resolve, delay));
return this.getServerConfig(id, retryCount + 1);
}
throw new Error(
`Failed to get server config after ${maxRetries + 1} attempts: ${
error instanceof Error ? error.message : "Unknown error"
}`
);
}
}
/**
* Listar ferramentas de um servidor específico
*/
static async getServerTools(id: string, retryCount = 0): Promise<MCPTool[]> {
const maxRetries = 3;
const baseDelay = 1000;
try {
// Verificar cache primeiro
const cacheKey = MCPCache.generateServerToolsKey(id);
const cachedResult = MCPCache.get(cacheKey);
if (cachedResult) {
console.log("Returning cached server tools");
return cachedResult;
}
const [owner, repo] = id.split("/");
if (!owner || !repo) {
throw new Error(
`Invalid MCP ID format: ${id}. Expected format: owner/repo`
);
}
const url = `${DEEPNLP_API_BASE}/tools/${owner}/${repo}`;
console.log(
`DeepNLP Server Tools Request (attempt ${retryCount + 1}):`,
url
);
const response = await fetch(url, {
method: "GET",
headers: {
Accept: "application/json",
"User-Agent": "Ollahub-MCP-Client/1.0",
},
signal: AbortSignal.timeout(10000),
});
if (!response.ok) {
throw new Error(
`DeepNLP Server Tools API error: ${response.status} ${response.statusText}`
);
}
const data = await response.json();
console.log("DeepNLP Server Tools Response:", data);
const tools = data.tools || [];
// Salvar no cache
MCPCache.setWithDataType(cacheKey, tools, "tools");
return tools;
} catch (error) {
console.error(
`Error getting server tools from DeepNLP (attempt ${retryCount + 1}):`,
error
);
if (retryCount < maxRetries) {
const delay = baseDelay * Math.pow(2, retryCount);
console.log(`Retrying server tools in ${delay}ms...`);
await new Promise((resolve) => setTimeout(resolve, delay));
return this.getServerTools(id, retryCount + 1);
}
throw new Error(
`Failed to get server tools after ${maxRetries + 1} attempts: ${
error instanceof Error ? error.message : "Unknown error"
}`
);
}
}
/**
* Transformar resposta DeepNLP para formato interno MCPProvider
*/
static transformToMCPProvider(deepnlpItem: any): MCPProvider {
const category = this.mapCategory(deepnlpItem.subfield || "other");
return {
id: deepnlpItem.id || "",
name: deepnlpItem.content_name || "",
author: deepnlpItem.publisher_id || "",
description: deepnlpItem.description || "",
version: "1.0.0", // DeepNLP não fornece versão específica
category,
tags: deepnlpItem.content_tag_list
? deepnlpItem.content_tag_list
.split(",")
.map((tag: string) => tag.trim())
: [],
rating: parseFloat(deepnlpItem.rating || "0"),
totalRatings: parseInt(deepnlpItem.review_cnt || "0"),
repository: deepnlpItem.website,
homepage: deepnlpItem.website,
installed: false, // Será verificado localmente
subfield: deepnlpItem.subfield,
field: deepnlpItem.field,
config: deepnlpItem.config || [],
tools: deepnlpItem.ext_info?.tools || [],
};
}
/**
* Mapear categoria DeepNLP para categoria interna
*/
static mapCategory(subfield: string): MCPProvider["category"] {
return this.reverseCategoryMap[subfield?.toLowerCase()] || "other";
}
/**
* Obter mapeamento de categoria interna para DeepNLP
*/
static getCategoryMapping(internalCategory: string): string | undefined {
const mappings =
this.categoryMap[internalCategory as keyof typeof this.categoryMap];
return mappings?.[0]; // Retorna a primeira categoria DeepNLP correspondente
}
/**
* Verificar se a API está disponível
*/
static async checkHealth(): Promise<boolean> {
try {
const response = await fetch(`${DEEPNLP_API_BASE}?count_per_page=1`, {
method: "GET",
headers: {
Accept: "application/json",
"User-Agent": "Ollahub-MCP-Client/1.0",
},
});
return response.ok;
} catch (error) {
console.error("DeepNLP API health check failed:", error);
return false;
}
}
}
-210
View File
@@ -1,210 +0,0 @@
/**
* 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);
}
-214
View File
@@ -1,214 +0,0 @@
/**
* Serviço de gerenciamento de ambientes isolados para servidores MCP
*/
import { mkdir, rm, access } from "fs/promises";
import path from "path";
import { MCP_CONFIG } from "@/lib/config/mcp";
import { commandExists, spawnWithProgress } from "@/lib/utils/system";
import type {
MCPEnvironment,
SystemDependencies,
} from "@/lib/types/mcp-installer";
export class MCPEnvironmentService {
/**
* Verifica dependências do sistema
*/
static async checkSystemDependencies(): Promise<SystemDependencies> {
const [node, npm, python, pip, cargo, git] = await Promise.all([
commandExists("node"),
commandExists("npm"),
commandExists("python3"),
commandExists("pip3"),
commandExists("cargo"),
commandExists("git"),
]);
return { node, npm, python, pip, cargo, git };
}
/**
* Garante que o diretório de ambiente existe
*/
static async ensureEnvironmentDirectory(
type: "npm" | "python" | "rust"
): Promise<string> {
let dir: string;
switch (type) {
case "npm":
dir = MCP_CONFIG.npmDir;
break;
case "python":
dir = MCP_CONFIG.pythonDir;
break;
case "rust":
dir = MCP_CONFIG.rustDir;
break;
}
try {
await mkdir(dir, { recursive: true });
console.log(`✓ Environment directory ensured: ${dir}`);
return dir;
} catch (error: any) {
throw new Error(
`Failed to create environment directory: ${error.message}`
);
}
}
/**
* Cria ambiente virtual Python para um MCP específico
*/
static async createPythonVenv(
mcpId: string,
onProgress?: (message: string) => void
): Promise<string> {
const venvPath = path.join(MCP_CONFIG.pythonDir, mcpId);
// Verificar se já existe
try {
await access(venvPath);
onProgress?.(`Virtual environment already exists at ${venvPath}`);
return venvPath;
} catch {
// Não existe, criar novo
}
onProgress?.("Creating Python virtual environment...");
try {
const result = await spawnWithProgress(
"python3",
["-m", "venv", venvPath],
{
onOutput: (data) => onProgress?.(data.trim()),
timeout: 60000, // 1 minuto para criar venv
}
);
if (result.exitCode !== 0) {
throw new Error(
`Failed to create venv: ${result.stderr || result.stdout}`
);
}
onProgress?.(`✓ Virtual environment created at ${venvPath}`);
return venvPath;
} catch (error: any) {
throw new Error(`Failed to create Python venv: ${error.message}`);
}
}
/**
* Obtém caminho do executável para um ambiente
*/
static getExecutablePath(
type: "npm" | "python" | "rust",
mcpId: string,
packageName?: string
): { executable: string; args: string[] } {
switch (type) {
case "npm":
// Para npm, usar npx para executar pacotes instalados localmente
return {
executable: "npx",
args: packageName ? ["--prefix", MCP_CONFIG.npmDir, packageName] : [],
};
case "python": {
const venvPath = path.join(MCP_CONFIG.pythonDir, mcpId);
const pythonBin =
process.platform === "win32"
? path.join(venvPath, "Scripts", "python.exe")
: path.join(venvPath, "bin", "python");
return {
executable: pythonBin,
args: packageName ? ["-m", packageName] : [],
};
}
case "rust": {
// Para Rust, o binário estará em target/release/
const binaryPath = path.join(
MCP_CONFIG.rustDir,
mcpId,
"target",
"release",
packageName || mcpId
);
return {
executable: binaryPath,
args: [],
};
}
}
}
/**
* Limpa ambiente de um MCP (desinstalação)
*/
static async cleanupEnvironment(environment: MCPEnvironment): Promise<void> {
try {
console.log(`Cleaning up environment at ${environment.path}`);
await rm(environment.path, { recursive: true, force: true });
console.log(`✓ Environment cleaned: ${environment.path}`);
} catch (error: any) {
console.error(`Warning: Failed to cleanup environment: ${error.message}`);
// Não lançar erro - cleanup é best-effort
}
}
/**
* Verifica se um ambiente está válido
*/
static async validateEnvironment(
environment: MCPEnvironment
): Promise<boolean> {
try {
// Verificar se o diretório existe
await access(environment.path);
// Verificar se o executável existe
await access(environment.executable);
return true;
} catch {
return false;
}
}
/**
* Obtém informações sobre o ambiente de um MCP
*/
static async getEnvironmentInfo(
mcpId: string,
type: "npm" | "python" | "rust"
): Promise<{
exists: boolean;
path: string;
size?: number;
}> {
const envPath = path.join(
type === "npm"
? MCP_CONFIG.npmDir
: type === "python"
? path.join(MCP_CONFIG.pythonDir, mcpId)
: path.join(MCP_CONFIG.rustDir, mcpId)
);
try {
await access(envPath);
// TODO: Calcular tamanho do diretório se necessário
return { exists: true, path: envPath };
} catch {
return { exists: false, path: envPath };
}
}
}
-342
View File
@@ -1,342 +0,0 @@
/**
* 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
// Se status é undefined/null, considerar como "ready" (MCPs instalados antes da implementação de status)
if (mcpData.status && mcpData.status !== "ready") {
throw new Error(
`MCP ${mcpId} is not ready (status: ${mcpData.status})`
);
}
// Obter comando executável
// mcpData.config já vem parseado do repositório, mas pode ser string em casos legados
const config =
typeof mcpData.config === "string"
? JSON.parse(mcpData.config)
: mcpData.config || {};
const commandFromConfig = config.command;
const argsFromConfig = Array.isArray(config.args) ? config.args : [];
let command = commandFromConfig;
let args = argsFromConfig;
// Se não houver command em config, reconstruir a partir de executable_command (se existir)
if (!command && mcpData.executable_command) {
const parts = String(mcpData.executable_command).trim().split(/\s+/);
command = parts[0];
args = parts.slice(1);
}
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 = {
...globalThis.process.env,
...(environmentPath && {
PATH: `${environmentPath}/node_modules/.bin:${globalThis.process.env.PATH}`,
}),
};
const childProcess = spawn(command, args, {
cwd: environmentPath || globalThis.process.cwd(),
env,
stdio: ["pipe", "pipe", "pipe"],
});
let stdout = "";
let stderr = "";
let responseReceived = false;
// Timeout
const timeout = setTimeout(() => {
if (!responseReceived) {
childProcess.kill();
reject(new Error(`Execution timeout after ${EXECUTION_TIMEOUT}ms`));
}
}, EXECUTION_TIMEOUT);
// Capturar stdout
childProcess.stdout?.on("data", (data) => {
stdout += data.toString();
// Loga por linhas para diagnóstico
const printedLines = data
.toString()
.split("\n")
.filter((l: string) => l.trim().length > 0);
for (const l of printedLines) {
console.log(`📥 [MCP stdout] ${l}`);
}
// Tentar parsear respostas JSON-RPC
const parsedLines = stdout.split("\n");
for (const line of parsedLines) {
if (line.trim()) {
try {
const parsed = JSON.parse(line);
console.log("🔎 [MCP stdout parsed]", parsed);
if (parsed.id === 2 && parsed.result) {
// Resposta do tools/call
clearTimeout(timeout);
responseReceived = true;
childProcess.kill();
resolve(parsed.result);
}
} catch (e) {
// Linha não é JSON válido, continuar
}
}
}
});
// Capturar stderr
childProcess.stderr?.on("data", (data) => {
stderr += data.toString();
const errLines = data
.toString()
.split("\n")
.filter((l: string) => l.trim().length > 0);
for (const l of errLines) {
console.warn(`⚠️ [MCP stderr] ${l}`);
}
});
// Erro de processo
childProcess.on("error", (error) => {
clearTimeout(timeout);
reject(new Error(`Process error: ${error.message}`));
});
// Processo encerrou
childProcess.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,
};
console.log("📤 [MCP send] initialize", initRequest);
childProcess.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,
};
console.log("📤 [MCP send] tools/call", toolCallRequest);
childProcess.stdin?.write(JSON.stringify(toolCallRequest) + "\n");
childProcess.stdin?.end();
}, 500);
} catch (error: any) {
clearTimeout(timeout);
childProcess.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;
// Tratar objeto vazio como "ausente" quando tipo esperado não é object
if (
value &&
actualType === "object" &&
!Array.isArray(value) &&
Object.keys(value).length === 0 &&
expectedType !== "object"
) {
continue;
}
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 };
}
}
}
-429
View File
@@ -1,429 +0,0 @@
/**
* Serviço de instalação de servidores MCP
*/
import path from "path";
import { MCP_CONFIG } from "@/lib/config/mcp";
import { MCPEnvironmentService } from "./mcp-environment";
import { sanitizePackageName, spawnWithProgress } from "@/lib/utils/system";
import type {
MCPEnvironment,
InstallationProgress,
MCPInstallationStatus,
InstallationConfig,
} from "@/lib/types/mcp-installer";
export class MCPInstallerService {
/**
* Instala um servidor MCP baseado na configuração
*/
static async install(
config: InstallationConfig,
onProgress: (progress: InstallationProgress) => void
): Promise<MCPEnvironment> {
// Verificar dependências do sistema
onProgress({
status: "checking_dependencies" as MCPInstallationStatus,
message: "Checking system dependencies...",
percentage: 5,
});
const dependencies = await MCPEnvironmentService.checkSystemDependencies();
// Determinar tipo de instalação
const installType = this.determineInstallType(config, dependencies);
onProgress({
status: "downloading" as MCPInstallationStatus,
message: `Preparing ${installType} installation...`,
percentage: 10,
});
// Delegar para instalador específico
switch (installType) {
case "npm":
return await this.installNPM(config, onProgress, dependencies);
case "python":
return await this.installPython(config, onProgress, dependencies);
case "rust":
return await this.installRust(config, onProgress, dependencies);
default:
throw new Error(`Unsupported installation type: ${installType}`);
}
}
/**
* Determina o tipo de instalação baseado na configuração
*/
private static determineInstallType(
config: InstallationConfig,
dependencies: any
): "npm" | "python" | "rust" {
// Preferir package_registry se disponível
if (config.packageRegistry === "npm" && dependencies.npm) {
return "npm";
}
if (config.packageRegistry === "pypi" && dependencies.python) {
return "python";
}
if (config.packageRegistry === "cargo" && dependencies.cargo) {
return "rust";
}
// Tentar detectar pelo nome do pacote
if (config.packageName) {
if (
config.packageName.startsWith("@") ||
config.packageName.includes("/")
) {
if (dependencies.npm) return "npm";
}
if (
config.packageName.includes("mcp-server") ||
config.packageName.includes("mcp_server")
) {
if (dependencies.python) return "python";
}
}
// Fallback para npm se disponível
if (dependencies.npm) return "npm";
if (dependencies.python) return "python";
if (dependencies.cargo) return "rust";
throw new Error("No suitable package manager found");
}
/**
* Instala pacote NPM
*/
private static async installNPM(
config: InstallationConfig,
onProgress: (progress: InstallationProgress) => void,
dependencies: any
): Promise<MCPEnvironment> {
if (!dependencies.npm) {
throw new Error("npm is not installed on this system");
}
// Sanitizar nome do pacote
const packageName = sanitizePackageName(config.packageName);
onProgress({
status: "installing" as MCPInstallationStatus,
message: `Installing npm package: ${packageName}`,
percentage: 20,
logs: config.enableLogs
? [`Installing ${packageName} via npm...`]
: undefined,
});
// Garantir diretório npm existe
const npmDir = await MCPEnvironmentService.ensureEnvironmentDirectory(
"npm"
);
// Instalar pacote localmente no diretório npm
const logs: string[] = [];
try {
const npmBin = process.platform === "win32" ? "npm.cmd" : "npm";
const result = await spawnWithProgress(
npmBin,
["install", packageName, "--prefix", npmDir],
{
cwd: npmDir,
timeout: MCP_CONFIG.installTimeout,
onOutput: (data, isError) => {
if (config.enableLogs) {
logs.push(data);
}
// Atualizar progresso baseado na saída
let percentage = 40;
if (data.includes("fetchMetadata")) percentage = 30;
if (data.includes("extract")) percentage = 50;
if (data.includes("finalize")) percentage = 70;
onProgress({
status: "installing" as MCPInstallationStatus,
message: isError
? `npm: ${data.substring(0, 100)}`
: `Installing: ${data.substring(0, 100)}`,
percentage,
logs: config.enableLogs ? [data] : undefined,
});
},
}
);
if (result.exitCode !== 0) {
throw new Error(
`npm install failed: ${result.stderr || result.stdout}`
);
}
onProgress({
status: "installing" as MCPInstallationStatus,
message: "npm package installed successfully",
percentage: 80,
logs: config.enableLogs ? ["✓ Installation completed"] : undefined,
});
// Construir ambiente
// Para NPM, executar npx do diretório onde foi instalado (não usa --prefix que não funciona)
const npxBin = process.platform === "win32" ? "npx.cmd" : "npx";
const environment: MCPEnvironment = {
type: "npm",
path: npmDir,
executable: npxBin,
args: ["-y", packageName],
};
return environment;
} catch (error: any) {
const msg = String(error?.message || "");
if (msg.includes("spawn EINVAL") || msg.includes("Failed to spawn process") || msg.includes("ENOENT")) {
const npxBin = process.platform === "win32" ? "npx.cmd" : "npx";
onProgress({
status: "installing" as MCPInstallationStatus,
message: "Falling back to npx runtime execution (no preinstall)",
percentage: 60,
logs: config.enableLogs ? [msg] : undefined,
});
const environment: MCPEnvironment = {
type: "npm",
path: npmDir,
executable: npxBin,
args: ["-y", packageName],
};
return environment;
}
throw new Error(`NPM installation failed: ${error.message}`);
}
}
/**
* Instala pacote Python
*/
private static async installPython(
config: InstallationConfig,
onProgress: (progress: InstallationProgress) => void,
dependencies: any
): Promise<MCPEnvironment> {
if (!dependencies.python || !dependencies.pip) {
throw new Error("Python3 and pip3 are not installed on this system");
}
const packageName = sanitizePackageName(config.packageName);
onProgress({
status: "installing" as MCPInstallationStatus,
message: "Creating Python virtual environment...",
percentage: 20,
logs: config.enableLogs ? ["Creating venv..."] : undefined,
});
// Garantir diretório python existe
await MCPEnvironmentService.ensureEnvironmentDirectory("python");
// Criar venv para este MCP
const venvPath = await MCPEnvironmentService.createPythonVenv(
config.mcpId,
(msg) => {
if (config.enableLogs) {
onProgress({
status: "installing" as MCPInstallationStatus,
message: msg,
percentage: 30,
logs: [msg],
});
}
}
);
onProgress({
status: "installing" as MCPInstallationStatus,
message: `Installing Python package: ${packageName}`,
percentage: 40,
logs: config.enableLogs
? [`Installing ${packageName} via pip...`]
: undefined,
});
// Caminho para pip no venv
const pipBin =
process.platform === "win32"
? path.join(venvPath, "Scripts", "pip.exe")
: path.join(venvPath, "bin", "pip");
const pythonBin =
process.platform === "win32"
? path.join(venvPath, "Scripts", "python.exe")
: path.join(venvPath, "bin", "python");
try {
const result = await spawnWithProgress(pipBin, ["install", packageName], {
cwd: venvPath,
timeout: MCP_CONFIG.installTimeout,
onOutput: (data, isError) => {
if (config.enableLogs) {
onProgress({
status: "installing" as MCPInstallationStatus,
message: `pip: ${data.substring(0, 100)}`,
percentage: 60,
logs: [data],
});
}
},
});
if (result.exitCode !== 0) {
throw new Error(
`pip install failed: ${result.stderr || result.stdout}`
);
}
onProgress({
status: "installing" as MCPInstallationStatus,
message: "Python package installed successfully",
percentage: 80,
logs: config.enableLogs ? ["✓ Installation completed"] : undefined,
});
// Extrair módulo Python do nome do pacote (ex: mcp-server-git -> mcp_server_git)
const moduleName = packageName.replace(/-/g, "_");
const environment: MCPEnvironment = {
type: "python",
path: venvPath,
executable: pythonBin,
args: ["-m", moduleName],
};
return environment;
} catch (error: any) {
throw new Error(`Python installation failed: ${error.message}`);
}
}
/**
* Instala servidor Rust (via git clone + cargo build)
*/
private static async installRust(
config: InstallationConfig,
onProgress: (progress: InstallationProgress) => void,
dependencies: any
): Promise<MCPEnvironment> {
if (!dependencies.cargo || !dependencies.git) {
throw new Error("Cargo and Git are not installed on this system");
}
if (!config.repositoryUrl) {
throw new Error("Repository URL is required for Rust installation");
}
onProgress({
status: "downloading" as MCPInstallationStatus,
message: "Cloning Rust repository...",
percentage: 20,
logs: config.enableLogs
? [`Cloning ${config.repositoryUrl}...`]
: undefined,
});
// Garantir diretório rust existe
await MCPEnvironmentService.ensureEnvironmentDirectory("rust");
const projectPath = path.join(MCP_CONFIG.rustDir, config.mcpId);
try {
// Clonar repositório
const cloneResult = await spawnWithProgress(
"git",
["clone", config.repositoryUrl, projectPath],
{
cwd: MCP_CONFIG.rustDir,
timeout: MCP_CONFIG.installTimeout,
onOutput: (data) => {
if (config.enableLogs) {
onProgress({
status: "downloading" as MCPInstallationStatus,
message: `git: ${data.substring(0, 100)}`,
percentage: 30,
logs: [data],
});
}
},
}
);
if (cloneResult.exitCode !== 0) {
throw new Error(`git clone failed: ${cloneResult.stderr}`);
}
onProgress({
status: "installing" as MCPInstallationStatus,
message: "Building Rust project...",
percentage: 40,
logs: config.enableLogs ? ["Building with cargo..."] : undefined,
});
// Compilar com cargo
const buildResult = await spawnWithProgress(
"cargo",
["build", "--release"],
{
cwd: projectPath,
timeout: MCP_CONFIG.installTimeout,
onOutput: (data, isError) => {
if (config.enableLogs) {
let percentage = 50;
if (data.includes("Compiling")) percentage = 60;
if (data.includes("Finished")) percentage = 80;
onProgress({
status: "installing" as MCPInstallationStatus,
message: `cargo: ${data.substring(0, 100)}`,
percentage,
logs: [data],
});
}
},
}
);
if (buildResult.exitCode !== 0) {
throw new Error(`cargo build failed: ${buildResult.stderr}`);
}
onProgress({
status: "installing" as MCPInstallationStatus,
message: "Rust project built successfully",
percentage: 80,
logs: config.enableLogs ? ["✓ Build completed"] : undefined,
});
// Determinar nome do binário
const binaryName = config.packageName || config.mcpId;
const binaryPath = path.join(
projectPath,
"target",
"release",
process.platform === "win32" ? `${binaryName}.exe` : binaryName
);
const environment: MCPEnvironment = {
type: "rust",
path: projectPath,
executable: binaryPath,
args: [],
};
return environment;
} catch (error: any) {
throw new Error(`Rust installation failed: ${error.message}`);
}
}
}
-301
View File
@@ -1,301 +0,0 @@
/**
* Serviço de validação de protocolo MCP (JSON-RPC)
*/
import { spawn } from "child_process";
import type {
MCPEnvironment,
ValidationResult,
} from "@/lib/types/mcp-installer";
import { MCP_CONFIG } from "@/lib/config/mcp";
export class MCPValidatorService {
/**
* Valida se um servidor MCP está funcionando corretamente
* Testa o protocolo JSON-RPC do MCP
*/
static async validateMCPServer(
environment: MCPEnvironment,
config: any,
timeoutMs: number = MCP_CONFIG.testTimeout
): Promise<ValidationResult> {
return new Promise((resolve) => {
const startTime = Date.now();
let hasResolved = false;
// Preparar comando e argumentos
const command = environment.executable;
const args = environment.args || [];
const env = {
...process.env,
...environment.env,
...(config.env || {}),
};
// Para NPM, adicionar node_modules/.bin ao PATH para encontrar binários locais
if (environment.type === "npm" && environment.path) {
const nodeBinPath = `${environment.path}/node_modules/.bin`;
env.PATH = `${nodeBinPath}:${env.PATH || process.env.PATH}`;
}
console.log(`🧪 Testing MCP server: ${command} ${args.join(" ")}`);
try {
// Spawnar processo do servidor MCP
const serverProcess = spawn(command, args, {
cwd: environment.path, // Executar do diretório de instalação
env,
stdio: ["pipe", "pipe", "pipe"],
shell: process.platform === "win32", // Workaround para *.cmd no Windows
});
let stdout = "";
let stderr = "";
const responses: any[] = [];
let currentBuffer = "";
// Timeout
const timeoutId = setTimeout(() => {
if (!hasResolved) {
hasResolved = true;
serverProcess.kill();
resolve({
success: false,
protocol: "unknown",
tools: [],
error: `Timeout after ${timeoutMs}ms. Server did not respond.`,
});
}
}, timeoutMs);
// Capturar stdout (respostas JSON-RPC)
serverProcess.stdout?.on("data", (data: Buffer) => {
const text = data.toString();
stdout += text;
currentBuffer += text;
// Processar respostas JSON-RPC linha por linha
const lines = currentBuffer.split("\n");
currentBuffer = lines.pop() || "";
for (const line of lines) {
const trimmed = line.trim();
if (trimmed.startsWith("{")) {
try {
const response = JSON.parse(trimmed);
responses.push(response);
console.log(
"📨 MCP Response:",
JSON.stringify(response).substring(0, 200)
);
// Verificar se recebemos resposta válida de initialize
if (
response.id === 1 &&
response.result &&
response.result.capabilities
) {
// Protocolo MCP detectado, agora listar ferramentas
this.sendRequest(serverProcess, {
jsonrpc: "2.0",
method: "tools/list",
id: 2,
});
}
// Verificar se recebemos lista de ferramentas
if (response.id === 2 && response.result) {
clearTimeout(timeoutId);
if (!hasResolved) {
hasResolved = true;
serverProcess.kill();
const tools = Array.isArray(response.result.tools)
? response.result.tools.map((tool: any) => ({
name: tool.name || "unnamed",
description: tool.description,
inputSchema: tool.inputSchema,
}))
: [];
resolve({
success: true,
protocol: "mcp",
tools,
capabilities: responses[0]?.result?.capabilities,
});
}
}
} catch (e) {
// Não é JSON válido, ignorar
}
}
}
});
// Capturar stderr
serverProcess.stderr?.on("data", (data: Buffer) => {
stderr += data.toString();
});
// Error handler
serverProcess.on("error", (error) => {
clearTimeout(timeoutId);
if (!hasResolved) {
hasResolved = true;
resolve({
success: false,
protocol: "unknown",
tools: [],
error: `Failed to spawn server: ${error.message}`,
});
}
});
// Exit handler
serverProcess.on("exit", (code) => {
clearTimeout(timeoutId);
if (!hasResolved) {
hasResolved = true;
// Se saiu muito rápido sem responder, é um erro
const elapsed = Date.now() - startTime;
if (elapsed < 1000) {
resolve({
success: false,
protocol: "unknown",
tools: [],
error: `Server exited quickly with code ${code}. stderr: ${stderr}`,
});
} else {
// Servidor pode ter processado e saído normalmente
resolve({
success: false,
protocol: "unknown",
tools: [],
error: `Server exited with code ${code} before completing validation`,
});
}
}
});
// Aguardar servidor estar pronto (pequeno delay)
setTimeout(() => {
if (!hasResolved) {
// Enviar requisição de initialize
this.sendRequest(serverProcess, {
jsonrpc: "2.0",
method: "initialize",
params: {
protocolVersion: "2024-11-05",
capabilities: {},
clientInfo: {
name: "ollahub",
version: "1.0.0",
},
},
id: 1,
});
}
}, 500);
} catch (error: any) {
resolve({
success: false,
protocol: "unknown",
tools: [],
error: `Validation error: ${error.message}`,
});
}
});
}
/**
* Envia requisição JSON-RPC para o servidor
*/
private static sendRequest(process: any, request: any): void {
try {
const requestStr = JSON.stringify(request) + "\n";
console.log("📤 Sending MCP request:", requestStr.trim());
process.stdin?.write(requestStr);
} catch (error: any) {
console.error("Failed to send request:", error.message);
}
}
/**
* Validação simples de servidor (apenas verifica se inicia)
* Usado como fallback se validação JSON-RPC falhar
*/
static async simpleValidation(
environment: MCPEnvironment,
config: any,
timeoutMs: number = 5000
): Promise<{ success: boolean; error?: string }> {
return new Promise((resolve) => {
const command = environment.executable;
const args = environment.args || [];
const env = {
...process.env,
...environment.env,
...(config.env || {}),
};
// Para NPM, adicionar node_modules/.bin ao PATH para encontrar binários locais
if (environment.type === "npm" && environment.path) {
const nodeBinPath = `${environment.path}/node_modules/.bin`;
env.PATH = `${nodeBinPath}:${env.PATH || process.env.PATH}`;
}
try {
const serverProcess = spawn(command, args, {
cwd: environment.path, // Executar do diretório de instalação
env,
stdio: ["pipe", "pipe", "pipe"],
shell: process.platform === "win32",
});
let hasResolved = false;
const timeoutId = setTimeout(() => {
if (!hasResolved) {
hasResolved = true;
serverProcess.kill();
// Se chegou ao timeout sem errar, provavelmente está rodando
resolve({ success: true });
}
}, timeoutMs);
serverProcess.on("error", (error) => {
clearTimeout(timeoutId);
if (!hasResolved) {
hasResolved = true;
resolve({
success: false,
error: `Failed to start: ${error.message}`,
});
}
});
serverProcess.on("exit", (code) => {
clearTimeout(timeoutId);
if (!hasResolved) {
hasResolved = true;
if (code === 0) {
resolve({ success: true });
} else {
resolve({
success: false,
error: `Exited with code ${code}`,
});
}
}
});
} catch (error: any) {
resolve({
success: false,
error: `Validation error: ${error.message}`,
});
}
});
}
}
-77
View File
@@ -1,77 +0,0 @@
/**
* 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:*",
];
}
-30
View File
@@ -1,30 +0,0 @@
export const DEFAULT_SYSTEM_PROMPT = `Você é um assistente de IA especializado em desenvolvimento de software e análise técnica.
FORMATAÇÃO OBRIGATÓRIA:
- Sempre use blocos de código markdown com especificação de linguagem
- Para JavaScript/TypeScript: \`\`\`javascript ou \`\`\`typescript
- Para Python: \`\`\`python
- Para Bash/Shell: \`\`\`bash
- Para SQL: \`\`\`sql
- Para JSON: \`\`\`json
- Para YAML: \`\`\`yaml
- Para Docker: \`\`\`dockerfile
- Para configurações: \`\`\`toml, \`\`\`ini, \`\`\`conf
ESTRUTURA DE RESPOSTAS:
1. Explicação conceitual clara
2. Exemplo prático em bloco de código
3. Explicação do código quando necessário
4. Boas práticas
5. Possíveis problemas e soluções
CONTEXTO OLLAHUB:
- Execução local via Ollama
- Stack frequente: Next.js, React, Node.js
- Priorize exemplos executáveis e explique comandos e configurações.`;
export function mergeSystemPrompt(userPrompt?: string) {
if (!userPrompt || userPrompt.trim().length === 0)
return DEFAULT_SYSTEM_PROMPT;
return `${DEFAULT_SYSTEM_PROMPT}\n\nDiretivas do usuário:\n${userPrompt}`;
}
-74
View File
@@ -1,74 +0,0 @@
/**
* 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[];
}
-65
View File
@@ -1,65 +0,0 @@
/**
* Tipos e interfaces para o sistema de instalação MCP
*/
export type PackageRegistry = "npm" | "pypi" | "cargo" | "other" | null;
export enum MCPInstallationStatus {
PENDING = "pending",
CHECKING_DEPENDENCIES = "checking_dependencies",
DOWNLOADING = "downloading",
INSTALLING = "installing",
TESTING = "testing",
READY = "ready",
FAILED = "failed",
}
export interface InstallationProgress {
status: MCPInstallationStatus;
message: string;
percentage: number;
logs?: string[];
}
export interface MCPEnvironment {
type: "npm" | "python" | "rust";
path: string;
executable: string;
args?: string[];
env?: Record<string, string>;
}
export interface ValidationResult {
success: boolean;
protocol: "mcp" | "unknown";
tools: Array<{
name: string;
description?: string;
inputSchema?: any;
}>;
resources?: Array<{
uri: string;
name: string;
description?: string;
}>;
error?: string;
capabilities?: any;
}
export interface SystemDependencies {
node: boolean;
npm: boolean;
python: boolean;
pip: boolean;
cargo: boolean;
git: boolean;
}
export interface InstallationConfig {
mcpId: string;
packageName: string;
packageRegistry: PackageRegistry;
repositoryUrl?: string;
config: any;
enableLogs: boolean;
}
-72
View File
@@ -1,72 +0,0 @@
export interface MCPProvider {
id: string; // formato: "owner/repo"
originalId?: string; // ID original do cache antes de transformar para owner/repo
owner?: string; // Owner do repositório
repo?: string; // Nome do repositório
name: string; // mapeado de content_name
author: string; // mapeado de publisher_id
description: string;
version: string;
category:
| "map"
| "browser"
| "office"
| "search"
| "database"
| "finance"
| "code"
| "chart"
| "payment"
| "other";
tags: string[]; // mapeado de content_tag_list
rating: number; // já existe na API
totalRatings: number; // mapeado de review_cnt
repository?: string; // mapeado de website
homepage?: string;
installed: boolean; // verificar localmente
// Novos campos da API DeepNLP
subfield?: string; // MAP, BROWSER, etc
field?: string; // sempre "MCP SERVER"
config?: any[]; // configuração JSON do servidor
tools?: MCPTool[]; // schemas das ferramentas
}
export interface MCPCategory {
id: string;
name: string;
icon: string;
count: number;
}
export interface MCPInstallationStatus {
success: boolean;
message: string;
mcpId: string;
}
export interface MCPTool {
name: string;
description: string;
input_schema: {
type: string;
properties: Record<string, any>;
required?: string[];
};
}
// Tipos relacionados a busca/marketplace foram removidos
export interface MCPTestResult {
success: boolean;
message: string;
responseTime?: number; // em ms
serverVersion?: string;
availableTools?: number;
error?: string;
}
export interface MCPTestRequest {
mcpId: string;
}
// Metadados específicos de PulseMCP removidos
-62
View File
@@ -1,62 +0,0 @@
import fs from "node:fs/promises";
import path from "node:path";
export type SavedRemoteModel = {
name: string; // full ref, e.g., owner/model:tag or model:tag
description?: string;
url?: string;
tags_count?: number;
installed?: boolean;
lastUsedAt?: string; // ISO
};
type SavedRemoteFile = {
models: SavedRemoteModel[];
};
function getFilePath() {
return path.join(process.cwd(), "database", "user-remote-models.json");
}
async function ensureFile(): Promise<void> {
const file = getFilePath();
try {
await fs.access(file);
} catch {
const initial: SavedRemoteFile = { models: [] };
await fs.mkdir(path.dirname(file), { recursive: true });
await fs.writeFile(file, JSON.stringify(initial, null, 2), "utf-8");
}
}
export async function getSavedRemoteModels(): Promise<SavedRemoteModel[]> {
await ensureFile();
const raw = await fs.readFile(getFilePath(), "utf-8");
try {
const json = JSON.parse(raw) as SavedRemoteFile;
return Array.isArray(json.models) ? json.models : [];
} catch {
return [];
}
}
export async function addSavedRemoteModel(entry: SavedRemoteModel): Promise<void> {
await ensureFile();
const current = await getSavedRemoteModels();
const nowIso = new Date().toISOString();
const idx = current.findIndex((m) => m.name === entry.name);
const merged: SavedRemoteModel = {
...entry,
installed: entry.installed ?? false,
lastUsedAt: entry.lastUsedAt ?? nowIso,
};
if (idx >= 0) {
current[idx] = { ...current[idx], ...merged, lastUsedAt: nowIso };
} else {
current.unshift(merged);
}
const toWrite: SavedRemoteFile = { models: current.slice(0, 500) };
await fs.writeFile(getFilePath(), JSON.stringify(toWrite, null, 2), "utf-8");
}
-166
View File
@@ -1,166 +0,0 @@
/**
* Utilitários para operações de sistema
*/
import { exec, spawn } from "child_process";
import { promisify } from "util";
const execAsync = promisify(exec);
/**
* Verifica se um comando existe no sistema
*/
export async function commandExists(command: string): Promise<boolean> {
try {
const isWindows = process.platform === "win32";
const checkCommand = isWindows ? "where" : "which";
await execAsync(`${checkCommand} ${command}`);
return true;
} catch {
return false;
}
}
/**
* Executa um comando e retorna stdout/stderr
*/
export async function execCommand(
cmd: string,
args: string[],
options?: {
cwd?: string;
env?: Record<string, string>;
timeout?: number;
}
): Promise<{ stdout: string; stderr: string; exitCode: number }> {
try {
const fullCommand = `${cmd} ${args.join(" ")}`;
const { stdout, stderr } = await execAsync(fullCommand, {
cwd: options?.cwd,
env: options?.env ? { ...process.env, ...options.env } : process.env,
timeout: options?.timeout || 300000, // 5 minutos default
});
return { stdout, stderr, exitCode: 0 };
} catch (error: any) {
return {
stdout: error.stdout || "",
stderr: error.stderr || error.message,
exitCode: error.code || 1,
};
}
}
/**
* Spawna um processo e captura output com progresso
*/
export async function spawnWithProgress(
cmd: string,
args: string[],
options: {
cwd?: string;
env?: Record<string, string>;
onOutput?: (data: string, isError: boolean) => void;
timeout?: number;
}
): Promise<{ exitCode: number; stdout: string; stderr: string }> {
return new Promise((resolve, reject) => {
let stdout = "";
let stderr = "";
let hasTimedOut = false;
const childProcess = spawn(cmd, args, {
cwd: options.cwd,
env: options.env ? { ...process.env, ...options.env } : process.env,
stdio: ["pipe", "pipe", "pipe"],
});
// Timeout handler
const timeout = options.timeout || 300000; // 5 minutos default
const timeoutId = setTimeout(() => {
hasTimedOut = true;
childProcess.kill("SIGTERM");
// Force kill após 5 segundos
setTimeout(() => {
if (!childProcess.killed) {
childProcess.kill("SIGKILL");
}
}, 5000);
}, timeout);
// Capturar stdout
childProcess.stdout?.on("data", (data: Buffer) => {
const text = data.toString();
stdout += text;
if (options.onOutput) {
options.onOutput(text, false);
}
});
// Capturar stderr
childProcess.stderr?.on("data", (data: Buffer) => {
const text = data.toString();
stderr += text;
if (options.onOutput) {
options.onOutput(text, true);
}
});
// Error handler
childProcess.on("error", (error) => {
clearTimeout(timeoutId);
reject(new Error(`Failed to spawn process: ${error.message}`));
});
// Exit handler
childProcess.on("close", (code) => {
clearTimeout(timeoutId);
if (hasTimedOut) {
reject(
new Error(
`Process timed out after ${timeout}ms. stdout: ${stdout}, stderr: ${stderr}`
)
);
} else {
resolve({
exitCode: code || 0,
stdout,
stderr,
});
}
});
});
}
/**
* Sanitiza nome de pacote para evitar command injection
* Suporta formatos: @scope/pkg, github:user/repo, git+https://..., etc.
*/
export function sanitizePackageName(packageName: string): string {
// URLs git+https:// devem ser preservadas completamente (são seguras)
if (
packageName.startsWith("git+https://") ||
packageName.startsWith("git+http://")
) {
// Apenas validar que parece uma URL válida
return packageName;
}
// Remove caracteres perigosos, mantém alphanumeric, @, :, +, /, -, _, .
// Suporta: @scope/pkg, github:user/repo
return packageName.replace(/[^a-zA-Z0-9@:+/\-_.]/g, "");
}
/**
* Valida se um caminho está dentro de um diretório base (previne path traversal)
*/
export function isPathSafe(basePath: string, targetPath: string): boolean {
const path = require("path");
const resolvedBase = path.resolve(basePath);
const resolvedTarget = path.resolve(targetPath);
return resolvedTarget.startsWith(resolvedBase);
}
-32
View File
@@ -1,32 +0,0 @@
import { generateEmbedding } from "./embeddings";
import { getDatabase } from "@/database/client";
import { EmbeddingRepository } from "@/database/repositories/embedding";
export async function semanticSearch(query: string, limit: number = 5) {
try {
const queryVector = await generateEmbedding(query);
const db = getDatabase();
const embeddingRepo = new EmbeddingRepository(db);
return await embeddingRepo.searchSimilar(queryVector, limit);
} catch (error) {
console.error("Erro na busca semântica:", error);
return [];
}
}
export async function addMessageEmbedding(
messageId: string,
content: string
): Promise<void> {
try {
const vector = await generateEmbedding(content);
const db = getDatabase();
const embeddingRepo = new EmbeddingRepository(db);
await embeddingRepo.create(messageId, vector);
} catch (error) {
console.error("Erro ao adicionar embedding:", error);
// Não falhar silenciosamente - embedding é opcional
}
}