feat(mcp): add GitHub repo support and tools display in MCP cards
- Support installing MCP servers from GitHub repos by converting 'github:user/repo' to 'git+https://github.com/user/repo.git' to avoid SSH issues and improve npm compatibility - Display available tools in MCPCard with badges, showing up to 6 tools and total count for better visibility of server capabilities - Remove unused onViewDetails prop and make uninstall button full-width for improved UI consistency
This commit is contained in:
1 parent
f19bf10ef3
commit
745746afa9
8 files changed
+227
-60
No files matched your search
@@ -144,14 +144,25 @@ function detectPackageInfo(serverConfig: any): {
|
|||||||
if (command === "npx" || command === "npm") {
|
if (command === "npx" || command === "npm") {
|
||||||
// npx @mcpcentral/mcp-time
|
// npx @mcpcentral/mcp-time
|
||||||
// npx -y @modelcontextprotocol/server-filesystem
|
// npx -y @modelcontextprotocol/server-filesystem
|
||||||
|
// npx -y github:user/repo
|
||||||
const packageArg = args.find(
|
const packageArg = args.find(
|
||||||
(arg: string) => arg.startsWith("@") || !arg.startsWith("-")
|
(arg: string) =>
|
||||||
|
arg.startsWith("@") || arg.startsWith("github:") || !arg.startsWith("-")
|
||||||
);
|
);
|
||||||
const packageName = packageArg || firstArg || "unknown-npm-package";
|
let packageName = packageArg || firstArg || "unknown-npm-package";
|
||||||
|
packageName = packageName.replace(/^-y\s*/, "").trim();
|
||||||
|
|
||||||
|
// Converter github:user/repo para git+https://github.com/user/repo.git
|
||||||
|
// Isso evita problemas com SSH e funciona melhor com npm install
|
||||||
|
if (packageName.startsWith("github:")) {
|
||||||
|
const repoPath = packageName.replace("github:", "");
|
||||||
|
packageName = `git+https://github.com/${repoPath}.git`;
|
||||||
|
console.log(`Converted github: format to: ${packageName}`);
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
packageRegistry: "npm",
|
packageRegistry: "npm",
|
||||||
packageName: packageName.replace(/^-y\s*/, "").trim(),
|
packageName,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+37
-12
@@ -18,7 +18,6 @@ interface MCPCardProps {
|
|||||||
mcp: MCPProvider;
|
mcp: MCPProvider;
|
||||||
onInstall: () => void;
|
onInstall: () => void;
|
||||||
onUninstall: () => void;
|
onUninstall: () => void;
|
||||||
onViewDetails: () => void;
|
|
||||||
mode?: "marketplace" | "installed";
|
mode?: "marketplace" | "installed";
|
||||||
onEditConfig?: () => void;
|
onEditConfig?: () => void;
|
||||||
onTestServer?: () => void;
|
onTestServer?: () => void;
|
||||||
@@ -35,7 +34,6 @@ export const MCPCard = memo(
|
|||||||
mcp,
|
mcp,
|
||||||
onInstall,
|
onInstall,
|
||||||
onUninstall,
|
onUninstall,
|
||||||
onViewDetails,
|
|
||||||
mode = "marketplace",
|
mode = "marketplace",
|
||||||
onEditConfig,
|
onEditConfig,
|
||||||
onTestServer,
|
onTestServer,
|
||||||
@@ -145,6 +143,41 @@ export const MCPCard = memo(
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Tools Badges */}
|
||||||
|
{mcp.tools && Array.isArray(mcp.tools) && mcp.tools.length > 0 && (
|
||||||
|
<div className="mb-3">
|
||||||
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
<Zap className="h-4 w-4 text-purple-500 dark:text-purple-400" />
|
||||||
|
<span className="text-xs font-semibold text-purple-600 dark:text-purple-400">
|
||||||
|
{mcp.tools.length} {mcp.tools.length === 1 ? "Tool" : "Tools"}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{mcp.tools.length > 6 && (
|
||||||
|
<span className="text-[10px] text-gray-500 dark:text-gray-400">
|
||||||
|
Showing 6 of {mcp.tools.length}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-wrap gap-1.5 max-h-[60px] overflow-hidden">
|
||||||
|
{mcp.tools.slice(0, 6).map((tool: any, index: number) => (
|
||||||
|
<span
|
||||||
|
key={index}
|
||||||
|
className="inline-flex items-center px-2.5 py-1 bg-gradient-to-r from-purple-50 to-purple-100 dark:from-purple-950/40 dark:to-purple-900/30 text-purple-700 dark:text-purple-300 text-[11px] font-medium rounded-md border border-purple-200/60 dark:border-purple-800/60 hover:border-purple-300 dark:hover:border-purple-700 transition-colors cursor-default shadow-sm"
|
||||||
|
title={tool.description || tool.name}
|
||||||
|
>
|
||||||
|
{tool.name}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
{mcp.tools.length > 6 && (
|
||||||
|
<span className="inline-flex items-center px-2.5 py-1 bg-gradient-to-r from-gray-50 to-gray-100 dark:from-gray-900/40 dark:to-gray-800/30 text-gray-600 dark:text-gray-400 text-[11px] font-medium rounded-md border border-gray-200/60 dark:border-gray-700/60 shadow-sm">
|
||||||
|
+{mcp.tools.length - 6}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Installation Progress */}
|
{/* Installation Progress */}
|
||||||
{installingStatus && (
|
{installingStatus && (
|
||||||
<div className="mb-3 p-3 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-md">
|
<div className="mb-3 p-3 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-md">
|
||||||
@@ -168,14 +201,6 @@ export const MCPCard = memo(
|
|||||||
|
|
||||||
{/* Actions */}
|
{/* Actions */}
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<button
|
|
||||||
onClick={onViewDetails}
|
|
||||||
className="flex-1 flex items-center justify-center gap-2 px-3 py-2 bg-[var(--background)] border border-[var(--border)] rounded-md hover:bg-[var(--surface)] transition-colors text-sm"
|
|
||||||
>
|
|
||||||
<Eye className="h-4 w-4" />
|
|
||||||
Details
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{mode === "installed" && onEditConfig ? (
|
{mode === "installed" && onEditConfig ? (
|
||||||
<>
|
<>
|
||||||
<button
|
<button
|
||||||
@@ -210,14 +235,14 @@ export const MCPCard = memo(
|
|||||||
) : mcp.installed ? (
|
) : mcp.installed ? (
|
||||||
<button
|
<button
|
||||||
onClick={onUninstall}
|
onClick={onUninstall}
|
||||||
className="px-3 py-2 bg-red-500 text-white rounded-md hover:bg-red-600 transition-colors text-sm mcp-install-button"
|
className="w-full px-3 py-2 bg-red-500 text-white rounded-md hover:bg-red-600 transition-colors text-sm mcp-install-button font-medium"
|
||||||
>
|
>
|
||||||
Uninstall
|
Uninstall
|
||||||
</button>
|
</button>
|
||||||
) : (
|
) : (
|
||||||
<button
|
<button
|
||||||
onClick={onInstall}
|
onClick={onInstall}
|
||||||
className="px-3 py-2 bg-[var(--primary)] text-[var(--primary-foreground)] rounded-md hover:bg-[var(--primary)]/90 transition-colors text-sm mcp-install-button"
|
className="w-full px-3 py-2 bg-[var(--primary)] text-[var(--primary-foreground)] rounded-md hover:bg-[var(--primary)]/90 transition-colors text-sm mcp-install-button font-medium"
|
||||||
>
|
>
|
||||||
Install
|
Install
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -38,7 +38,6 @@ export function MCPMarketplaceModal({
|
|||||||
"rating" | "name" | "recent" | "total_ratings" | "updated_at"
|
"rating" | "name" | "recent" | "total_ratings" | "updated_at"
|
||||||
>("rating");
|
>("rating");
|
||||||
const [sortOrder, setSortOrder] = useState<"asc" | "desc">("desc");
|
const [sortOrder, setSortOrder] = useState<"asc" | "desc">("desc");
|
||||||
const [selectedMCP, setSelectedMCP] = useState<MCPProvider | null>(null);
|
|
||||||
const [configModalOpen, setConfigModalOpen] = useState(false);
|
const [configModalOpen, setConfigModalOpen] = useState(false);
|
||||||
const [selectedConfigMCP, setSelectedConfigMCP] =
|
const [selectedConfigMCP, setSelectedConfigMCP] =
|
||||||
useState<MCPProvider | null>(null);
|
useState<MCPProvider | null>(null);
|
||||||
@@ -170,6 +169,32 @@ export function MCPMarketplaceModal({
|
|||||||
if (data.success) {
|
if (data.success) {
|
||||||
setInstalledMcps(data.mcps);
|
setInstalledMcps(data.mcps);
|
||||||
console.log("📦 Installed MCPs loaded:", data.mcps.length);
|
console.log("📦 Installed MCPs loaded:", data.mcps.length);
|
||||||
|
|
||||||
|
// Atualizar MCPs do marketplace para marcar como instalados
|
||||||
|
setMcps((prevMcps) =>
|
||||||
|
prevMcps.map((mcp) => {
|
||||||
|
const installedMatch = data.mcps.find(
|
||||||
|
(installed: any) =>
|
||||||
|
installed.id === mcp.id ||
|
||||||
|
installed.repo === mcp.repo ||
|
||||||
|
installed.name === mcp.name
|
||||||
|
);
|
||||||
|
|
||||||
|
if (installedMatch) {
|
||||||
|
return {
|
||||||
|
...mcp,
|
||||||
|
installed: true,
|
||||||
|
tools: installedMatch.tools || mcp.tools, // Atualizar tools também
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return { ...mcp, installed: false };
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`📊 Marketplace updated: ${data.mcps.length} MCPs marked as installed`
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
console.error("Failed to fetch installed MCPs:", data.error);
|
console.error("Failed to fetch installed MCPs:", data.error);
|
||||||
}
|
}
|
||||||
@@ -385,6 +410,8 @@ export function MCPMarketplaceModal({
|
|||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
|
console.log(`✅ Installation started for ${finalId}`);
|
||||||
|
|
||||||
// Adicionar ao tracking de instalação
|
// Adicionar ao tracking de instalação
|
||||||
setInstallingMcps((prev) => {
|
setInstallingMcps((prev) => {
|
||||||
const next = new Map(prev);
|
const next = new Map(prev);
|
||||||
@@ -396,20 +423,24 @@ export function MCPMarketplaceModal({
|
|||||||
return next;
|
return next;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Atualizar estado local
|
// Marcar como "instalando" no marketplace (não como instalado ainda)
|
||||||
|
// O polling vai atualizar para installed: true quando status === "ready"
|
||||||
setMcps((prev) =>
|
setMcps((prev) =>
|
||||||
prev.map((mcp) =>
|
prev.map((mcp) => {
|
||||||
mcp.id === selectedConfigMCP.id
|
// Verificar se é o mesmo MCP (por ID ou repo)
|
||||||
? { ...mcp, installed: true }
|
const isSameMcp =
|
||||||
: mcp
|
mcp.id === selectedConfigMCP.id ||
|
||||||
)
|
mcp.id === finalId ||
|
||||||
|
mcp.repo === selectedConfigMCP.repo;
|
||||||
|
|
||||||
|
return isSameMcp ? { ...mcp, installed: false } : mcp;
|
||||||
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
// Fechar modal
|
// Fechar modal
|
||||||
setConfigModalOpen(false);
|
setConfigModalOpen(false);
|
||||||
setSelectedConfigMCP(null);
|
setSelectedConfigMCP(null);
|
||||||
setMcpConfig(null);
|
setMcpConfig(null);
|
||||||
|
|
||||||
console.log(`✅ Installation started for ${finalId}`);
|
|
||||||
} else {
|
} else {
|
||||||
console.error("Error installing MCP:", data.error);
|
console.error("Error installing MCP:", data.error);
|
||||||
}
|
}
|
||||||
@@ -429,22 +460,36 @@ export function MCPMarketplaceModal({
|
|||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
// Atualizar estado local
|
console.log(`✅ MCP ${mcpId} uninstalled successfully`);
|
||||||
|
|
||||||
|
// Remover do tracking de instalação (se estiver lá)
|
||||||
|
setInstallingMcps((prev) => {
|
||||||
|
const next = new Map(prev);
|
||||||
|
next.delete(mcpId);
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Atualizar marketplace: marcar como não instalado
|
||||||
setMcps((prev) =>
|
setMcps((prev) =>
|
||||||
prev.map((mcp) =>
|
prev.map((mcp) =>
|
||||||
mcp.id === mcpId ? { ...mcp, installed: false } : mcp
|
mcp.id === mcpId || mcp.repo === mcpId
|
||||||
|
? { ...mcp, installed: false }
|
||||||
|
: mcp
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Recarregar lista de instalados para sincronizar
|
||||||
|
await fetchInstalledMCPs();
|
||||||
|
|
||||||
|
console.log(`📦 Lists updated after uninstalling ${mcpId}`);
|
||||||
|
} else {
|
||||||
|
console.error("Failed to uninstall MCP:", data.error);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error uninstalling MCP:", error);
|
console.error("Error uninstalling MCP:", error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleViewDetails = (mcp: MCPProvider) => {
|
|
||||||
setSelectedMCP(mcp);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Carregar MCPs e categorias quando o modal abrir
|
// Carregar MCPs e categorias quando o modal abrir
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isOpen) {
|
if (isOpen) {
|
||||||
@@ -454,11 +499,51 @@ export function MCPMarketplaceModal({
|
|||||||
}, [isOpen]); // Removido fetchMCPs das dependências
|
}, [isOpen]); // Removido fetchMCPs das dependências
|
||||||
|
|
||||||
// Carregar MCPs instalados quando aba "installed" for ativada
|
// Carregar MCPs instalados quando aba "installed" for ativada
|
||||||
|
// Carregar MCPs instalados sempre que o modal abrir (não apenas na aba instalados)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isOpen && activeTab === "installed") {
|
if (isOpen) {
|
||||||
fetchInstalledMCPs();
|
fetchInstalledMCPs();
|
||||||
|
|
||||||
|
// Limpar MCPs órfãos no tracking (que estão instalados mas ainda no Map)
|
||||||
|
// Isso previne polling infinito de MCPs que já completaram instalação
|
||||||
|
setTimeout(async () => {
|
||||||
|
if (installingMcps.size > 0) {
|
||||||
|
console.log(
|
||||||
|
`🔍 Checking for orphaned installations: ${installingMcps.size} tracked`
|
||||||
|
);
|
||||||
|
|
||||||
|
const installedResponse = await fetch("/api/mcp/installed");
|
||||||
|
const installedData = await installedResponse.json();
|
||||||
|
|
||||||
|
if (installedData.success) {
|
||||||
|
const installedIds = new Set(
|
||||||
|
installedData.mcps.map((m: any) => m.id)
|
||||||
|
);
|
||||||
|
|
||||||
|
setInstallingMcps((prev) => {
|
||||||
|
const next = new Map(prev);
|
||||||
|
let removed = 0;
|
||||||
|
|
||||||
|
for (const mcpId of prev.keys()) {
|
||||||
|
// Se já está instalado, remover do tracking
|
||||||
|
if (installedIds.has(mcpId)) {
|
||||||
|
next.delete(mcpId);
|
||||||
|
removed++;
|
||||||
|
console.log(`🧹 Removed orphaned tracking for ${mcpId}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (removed > 0) {
|
||||||
|
console.log(`✅ Cleaned ${removed} orphaned installations`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 1000); // Aguardar 1 segundo após abrir modal
|
||||||
}
|
}
|
||||||
}, [isOpen, activeTab]);
|
}, [isOpen, fetchInstalledMCPs]);
|
||||||
|
|
||||||
// Debounce para searchTerm
|
// Debounce para searchTerm
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -503,6 +588,10 @@ export function MCPMarketplaceModal({
|
|||||||
const isComplete = status === "ready" || status === "failed";
|
const isComplete = status === "ready" || status === "failed";
|
||||||
|
|
||||||
if (isComplete) {
|
if (isComplete) {
|
||||||
|
console.log(
|
||||||
|
`🎉 Installation ${status} for ${mcpId}, updating lists...`
|
||||||
|
);
|
||||||
|
|
||||||
// Remover do tracking
|
// Remover do tracking
|
||||||
setInstallingMcps((prev) => {
|
setInstallingMcps((prev) => {
|
||||||
const next = new Map(prev);
|
const next = new Map(prev);
|
||||||
@@ -510,11 +599,30 @@ export function MCPMarketplaceModal({
|
|||||||
return next;
|
return next;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Atualizar lista de MCPs
|
// Se instalação foi bem-sucedida, marcar como instalado imediatamente
|
||||||
await fetchMCPs();
|
if (status === "ready") {
|
||||||
if (activeTab === "installed") {
|
const toolsCount = data.validationResult?.tools?.length || 0;
|
||||||
await fetchInstalledMCPs();
|
console.log(
|
||||||
|
`✨ Marking ${mcpId} as installed with ${toolsCount} tools`
|
||||||
|
);
|
||||||
|
|
||||||
|
setMcps((prev) =>
|
||||||
|
prev.map((mcp) =>
|
||||||
|
mcp.id === mcpId || mcp.repo === mcpId || mcp.name === mcpId
|
||||||
|
? {
|
||||||
|
...mcp,
|
||||||
|
installed: true,
|
||||||
|
tools: data.validationResult?.tools || mcp.tools,
|
||||||
|
}
|
||||||
|
: mcp
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Recarregar lista de instalados sempre (não apenas na aba)
|
||||||
|
await fetchInstalledMCPs();
|
||||||
|
|
||||||
|
console.log(`✅ Lists synchronized after ${status} for ${mcpId}`);
|
||||||
} else {
|
} else {
|
||||||
// Atualizar progresso
|
// Atualizar progresso
|
||||||
setInstallingMcps((prev) => {
|
setInstallingMcps((prev) => {
|
||||||
@@ -527,9 +635,26 @@ export function MCPMarketplaceModal({
|
|||||||
return next;
|
return next;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// MCP não encontrado ou erro no servidor
|
||||||
|
console.warn(`⚠️ MCP ${mcpId} not found or error: ${data.error}`);
|
||||||
|
|
||||||
|
// Remover do tracking após erro (evita polling infinito)
|
||||||
|
setInstallingMcps((prev) => {
|
||||||
|
const next = new Map(prev);
|
||||||
|
next.delete(mcpId);
|
||||||
|
return next;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error polling status for ${mcpId}:`, error);
|
console.error(`❌ Error polling status for ${mcpId}:`, error);
|
||||||
|
|
||||||
|
// Remover do tracking após erro de rede (evita polling infinito)
|
||||||
|
setInstallingMcps((prev) => {
|
||||||
|
const next = new Map(prev);
|
||||||
|
next.delete(mcpId);
|
||||||
|
return next;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, 2000); // Poll a cada 2 segundos
|
}, 2000); // Poll a cada 2 segundos
|
||||||
@@ -734,7 +859,6 @@ export function MCPMarketplaceModal({
|
|||||||
mcp={mcp}
|
mcp={mcp}
|
||||||
onInstall={() => handleInstall(mcp.id)}
|
onInstall={() => handleInstall(mcp.id)}
|
||||||
onUninstall={() => handleUninstall(mcp.id)}
|
onUninstall={() => handleUninstall(mcp.id)}
|
||||||
onViewDetails={() => handleViewDetails(mcp)}
|
|
||||||
installingStatus={installingMcps.get(mcp.id)}
|
installingStatus={installingMcps.get(mcp.id)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
@@ -787,25 +911,6 @@ export function MCPMarketplaceModal({
|
|||||||
onTestServer={() => handleTestServer(mcp.id)}
|
onTestServer={() => handleTestServer(mcp.id)}
|
||||||
isTesting={testingMCP === mcp.id}
|
isTesting={testingMCP === mcp.id}
|
||||||
onUninstall={() => handleUninstall(mcp.id)}
|
onUninstall={() => handleUninstall(mcp.id)}
|
||||||
onViewDetails={() =>
|
|
||||||
handleViewDetails({
|
|
||||||
id: mcp.id,
|
|
||||||
name: mcp.name,
|
|
||||||
author: mcp.author || mcp.owner || "Unknown",
|
|
||||||
description:
|
|
||||||
mcp.description || `MCP instalado: ${mcp.name}`,
|
|
||||||
version: "1.0.0",
|
|
||||||
category: mcp.category || "other",
|
|
||||||
tags: mcp.tags || [],
|
|
||||||
rating: mcp.rating || 0,
|
|
||||||
totalRatings: mcp.totalRatings || 0,
|
|
||||||
installed: true,
|
|
||||||
repository: mcp.repository || "",
|
|
||||||
homepage: mcp.homepage || "",
|
|
||||||
config: mcp.config,
|
|
||||||
tools: mcp.tools,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
installingStatus={installingMcps.get(mcp.id)}
|
installingStatus={installingMcps.get(mcp.id)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@@ -170,11 +170,12 @@ export class MCPInstallerService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Construir ambiente
|
// Construir ambiente
|
||||||
|
// Para NPM, executar npx do diretório onde foi instalado (não usa --prefix que não funciona)
|
||||||
const environment: MCPEnvironment = {
|
const environment: MCPEnvironment = {
|
||||||
type: "npm",
|
type: "npm",
|
||||||
path: npmDir,
|
path: npmDir,
|
||||||
executable: "npx",
|
executable: "npx",
|
||||||
args: ["--prefix", npmDir, packageName],
|
args: [packageName],
|
||||||
};
|
};
|
||||||
|
|
||||||
return environment;
|
return environment;
|
||||||
|
|||||||
@@ -32,11 +32,18 @@ export class MCPValidatorService {
|
|||||||
...(config.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(" ")}`);
|
console.log(`🧪 Testing MCP server: ${command} ${args.join(" ")}`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Spawnar processo do servidor MCP
|
// Spawnar processo do servidor MCP
|
||||||
const serverProcess = spawn(command, args, {
|
const serverProcess = spawn(command, args, {
|
||||||
|
cwd: environment.path, // Executar do diretório de instalação
|
||||||
env,
|
env,
|
||||||
stdio: ["pipe", "pipe", "pipe"],
|
stdio: ["pipe", "pipe", "pipe"],
|
||||||
});
|
});
|
||||||
@@ -232,8 +239,15 @@ export class MCPValidatorService {
|
|||||||
...(config.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 {
|
try {
|
||||||
const serverProcess = spawn(command, args, {
|
const serverProcess = spawn(command, args, {
|
||||||
|
cwd: environment.path, // Executar do diretório de instalação
|
||||||
env,
|
env,
|
||||||
stdio: ["pipe", "pipe", "pipe"],
|
stdio: ["pipe", "pipe", "pipe"],
|
||||||
});
|
});
|
||||||
|
|||||||
+13
-2
@@ -137,10 +137,21 @@ export async function spawnWithProgress(
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Sanitiza nome de pacote para evitar command injection
|
* Sanitiza nome de pacote para evitar command injection
|
||||||
|
* Suporta formatos: @scope/pkg, github:user/repo, git+https://..., etc.
|
||||||
*/
|
*/
|
||||||
export function sanitizePackageName(packageName: string): string {
|
export function sanitizePackageName(packageName: string): string {
|
||||||
// Remove caracteres perigosos, mantém apenas alphanumeric, @, /, -, _
|
// URLs git+https:// devem ser preservadas completamente (são seguras)
|
||||||
return packageName.replace(/[^a-zA-Z0-9@/\-_.]/g, "");
|
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, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in new issue
Block a user