ModelContextProtocol (MCP) Full Compatibility (#3547)

* WIP MCP full compatibility layer

* implement MCP agent function wrapping and invocation methods

* Add `uvx` to docker bin for MCP executions

* dev build

* prune removed data

* Wrap MCP servers to lazy load items to not block the UI
Mobile bug fixes

* arm64 test build

* reset dev builder

* remove unused prop
This commit is contained in:
Timothy Carambat 2025-03-31 14:15:19 -07:00 committed by GitHub
parent bd9b7e812d
commit 7a01298a8e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 1666 additions and 49 deletions

View File

@ -6,7 +6,7 @@ concurrency:
on:
push:
branches: ['3157-feat-prompt-variables'] # put your current branch to create a build. Core team only.
branches: ['3000-mcp-compatibility'] # put your current branch to create a build. Core team only.
paths-ignore:
- '**.md'
- 'cloud-deployments/*'
@ -43,6 +43,10 @@ jobs:
fi
id: dockerhub
# Uncomment this + add linux/arm64 to platforms if you want to build for arm64 as well
# - name: Set up QEMU
# uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
with:
@ -74,6 +78,7 @@ jobs:
sbom: true
provenance: mode=max
platforms: linux/amd64
# platforms: linux/amd64,linux/arm64
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha

View File

@ -24,10 +24,16 @@ RUN DEBIAN_FRONTEND=noninteractive apt-get update && \
curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg && \
echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_18.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list && \
apt-get update && \
# Install node and yarn
apt-get install -yq --no-install-recommends nodejs && \
curl -LO https://github.com/yarnpkg/yarn/releases/download/v1.22.19/yarn_1.22.19_all.deb \
&& dpkg -i yarn_1.22.19_all.deb \
&& rm yarn_1.22.19_all.deb && \
# Install uvx (pinned to 0.6.10) for MCP support
curl -LsSf https://astral.sh/uv/0.6.10/install.sh | sh && \
mv /root/.local/bin/uv /usr/local/bin/uv && \
mv /root/.local/bin/uvx /usr/local/bin/uvx && \
echo "Installed uvx! $(uv --version)" && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
@ -84,10 +90,16 @@ RUN DEBIAN_FRONTEND=noninteractive apt-get update && \
curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg && \
echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_18.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list && \
apt-get update && \
# Install node and yarn
apt-get install -yq --no-install-recommends nodejs && \
curl -LO https://github.com/yarnpkg/yarn/releases/download/v1.22.19/yarn_1.22.19_all.deb \
&& dpkg -i yarn_1.22.19_all.deb \
&& rm yarn_1.22.19_all.deb && \
# Install uvx (pinned to 0.6.10) for MCP support
curl -LsSf https://astral.sh/uv/0.6.10/install.sh | sh && \
mv /root/.local/bin/uv /usr/local/bin/uv && \
mv /root/.local/bin/uvx /usr/local/bin/uvx && \
echo "Installed uvx! $(uv --version)" && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
@ -154,12 +166,6 @@ RUN chown -R anythingllm:anythingllm /app/server && \
chown -R anythingllm:anythingllm /app/collector
USER anythingllm
# No longer needed? (deprecated)
# WORKDIR /app/server
# RUN npx prisma generate --schema=./prisma/schema.prisma && \
# npx prisma migrate deploy --schema=./prisma/schema.prisma
# WORKDIR /app
# Setup the environment
ENV NODE_ENV=production
ENV ANYTHING_LLM_RUNTIME=docker

View File

@ -0,0 +1 @@
<svg version="1.2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 184 195" width="184" height="195"><style>.a{fill:none;stroke:#fff;stroke-linecap:round;stroke-width:12}</style><path fill-rule="evenodd" class="a" d="m25 97.9l67.9-67.9c9.4-9.4 24.6-9.4 33.9 0 9.4 9.3 9.4 24.5 0 33.9l-51.2 51.3"/><path fill-rule="evenodd" class="a" d="m76.3 114.5l50.5-50.6c9.4-9.4 24.6-9.4 34 0l0.3 0.4c9.4 9.3 9.4 24.5 0 33.9l-61.4 61.4c-3.1 3.1-3.1 8.2 0 11.3l12.6 12.6"/><path fill-rule="evenodd" class="a" d="m109.9 46.9l-50.3 50.2c-9.3 9.4-9.3 24.6 0 34 9.4 9.4 24.6 9.4 34 0l50.2-50.2"/></svg>

After

Width:  |  Height:  |  Size: 584 B

View File

@ -0,0 +1,76 @@
import { API_BASE } from "@/utils/constants";
import { baseHeaders } from "@/utils/request";
const MCPServers = {
/**
* Forces a reload of the MCP Hypervisor and its servers
* @returns {Promise<{success: boolean, error: string | null, servers: Array<{name: string, running: boolean, tools: Array<{name: string, description: string, inputSchema: Object}>, error: string | null, process: {pid: number, cmd: string} | null}>}>}
*/
forceReload: async () => {
return await fetch(`${API_BASE}/mcp-servers/force-reload`, {
method: "GET",
headers: baseHeaders(),
})
.then((res) => res.json())
.catch((e) => ({
servers: [],
success: false,
error: e.message,
}));
},
/**
* List all available MCP servers in the system
* @returns {Promise<{success: boolean, error: string | null, servers: Array<{name: string, running: boolean, tools: Array<{name: string, description: string, inputSchema: Object}>, error: string | null, process: {pid: number, cmd: string} | null}>}>}
*/
listServers: async () => {
return await fetch(`${API_BASE}/mcp-servers/list`, {
method: "GET",
headers: baseHeaders(),
})
.then((res) => res.json())
.catch((e) => ({
success: false,
error: e.message,
servers: [],
}));
},
/**
* Toggle the MCP server (start or stop)
* @param {string} name - The name of the MCP server to toggle
* @returns {Promise<{success: boolean, error: string | null}>}
*/
toggleServer: async (name) => {
return await fetch(`${API_BASE}/mcp-servers/toggle`, {
method: "POST",
headers: baseHeaders(),
body: JSON.stringify({ name }),
})
.then((res) => res.json())
.catch((e) => ({
success: false,
error: e.message,
}));
},
/**
* Delete the MCP server - will also remove it from the config file
* @param {string} name - The name of the MCP server to delete
* @returns {Promise<{success: boolean, error: string | null}>}
*/
deleteServer: async (name) => {
return await fetch(`${API_BASE}/mcp-servers/delete`, {
method: "POST",
headers: baseHeaders(),
body: JSON.stringify({ name }),
})
.then((res) => res.json())
.catch((e) => ({
success: false,
error: e.message,
}));
},
};
export default MCPServers;

View File

@ -22,7 +22,7 @@ export default function AgentFlowsList({
}
return (
<div className="bg-theme-bg-secondary text-white rounded-xl min-w-[360px] w-fit">
<div className="bg-theme-bg-secondary text-white rounded-xl w-full md:min-w-[360px]">
{flows.map((flow, index) => (
<div
key={flow.uuid}

View File

@ -1,5 +1,4 @@
import { CaretRight } from "@phosphor-icons/react";
import { isMobile } from "react-device-detect";
import { sentenceCase } from "text-case";
export default function ImportedSkillList({
@ -27,9 +26,7 @@ export default function ImportedSkillList({
return (
<div
className={`bg-theme-bg-secondary text-white rounded-xl ${
isMobile ? "w-full" : "min-w-[360px] w-fit"
}`}
className={`bg-theme-bg-secondary text-white rounded-xl w-full md:min-w-[360px]`}
>
{skills.map((config, index) => (
<div

View File

@ -0,0 +1,240 @@
import React, { useState, useEffect, useRef } from "react";
import showToast from "@/utils/toast";
import { CaretDown, Gear } from "@phosphor-icons/react";
import MCPLogo from "@/media/agents/mcp-logo.svg";
import { titleCase } from "text-case";
import truncate from "truncate";
import MCPServers from "@/models/mcpServers";
import pluralize from "pluralize";
function ManageServerMenu({ server, toggleServer, onDelete }) {
const [open, setOpen] = useState(false);
const [running, setRunning] = useState(server.running);
const menuRef = useRef(null);
async function deleteServer() {
if (
!window.confirm(
"Are you sure you want to delete this MCP server? It will be removed from your config file and you will need to add it back manually."
)
)
return;
const { success, error } = await MCPServers.deleteServer(server.name);
if (success) {
showToast("MCP server deleted successfully.", "success");
onDelete(server.name);
} else {
showToast(error || "Failed to delete MCP server.", "error");
}
}
async function handleToggleServer() {
if (
!window.confirm(
running
? "Are you sure you want to stop this MCP server? It will be started automatically when you next start the server."
: "Are you sure you want to start this MCP server? It will be started automatically when you next start the server."
)
)
return;
const { success, error } = await MCPServers.toggleServer(server.name);
if (success) {
setRunning(!running);
toggleServer(server.name);
showToast(
`MCP server ${server.name} ${running ? "started" : "stopped"} successfully.`,
"success",
{ clear: true }
);
} else {
showToast(error || "Failed to toggle MCP server.", "error", {
clear: true,
});
}
}
useEffect(() => {
const handleClickOutside = (event) => {
if (menuRef.current && !menuRef.current.contains(event.target)) {
setOpen(false);
}
};
document.addEventListener("mousedown", handleClickOutside);
return () => {
document.removeEventListener("mousedown", handleClickOutside);
};
}, []);
return (
<div className="relative" ref={menuRef}>
<button
type="button"
onClick={() => setOpen(!open)}
className="p-1.5 rounded-lg text-white hover:bg-theme-action-menu-item-hover transition-colors duration-300"
>
<Gear className="h-5 w-5" weight="bold" />
</button>
{open && (
<div className="absolute w-[150px] top-1 left-7 mt-1 border-[1.5px] border-white/40 rounded-lg bg-theme-action-menu-bg flex flex-col shadow-[0_4px_14px_rgba(0,0,0,0.25)] text-white z-99 md:z-10">
<button
type="button"
onClick={handleToggleServer}
className="border-none flex items-center rounded-lg gap-x-2 hover:bg-theme-action-menu-item-hover py-1.5 px-2 transition-colors duration-200 w-full text-left"
>
<span className="text-sm">
{running ? "Stop MCP Server" : "Start MCP Server"}
</span>
</button>
<button
type="button"
onClick={deleteServer}
className="border-none flex items-center rounded-lg gap-x-2 hover:bg-theme-action-menu-item-hover py-1.5 px-2 transition-colors duration-200 w-full text-left"
>
<span className="text-sm">Delete MCP Server</span>
</button>
</div>
)}
</div>
);
}
export default function ServerPanel({ server, toggleServer, onDelete }) {
return (
<>
<div className="p-2">
<div className="flex flex-col gap-y-[18px] max-w-[800px]">
<div className="flex w-full justify-between">
<div className="flex items-center gap-x-2">
<img src={MCPLogo} className="w-6 h-6 light:invert" />
<label htmlFor="name" className="text-white text-md font-bold">
{titleCase(server.name.replace(/[_-]/g, " "))}
</label>
{server.tools.length > 0 && (
<p className="text-theme-text-secondary text-sm">
{server.tools.length} {pluralize("tool", server.tools.length)}{" "}
available
</p>
)}
</div>
<ManageServerMenu
key={server.name}
server={server}
toggleServer={toggleServer}
onDelete={onDelete}
/>
</div>
<RenderServerConfig config={server.config} />
<RenderServerStatus server={server} />
<RenderServerTools tools={server.tools} />
</div>
</div>
</>
);
}
function RenderServerConfig({ config = null }) {
if (!config) return null;
return (
<div className="flex flex-col gap-y-2">
<p className="text-theme-text-primary text-sm">Startup Command</p>
<div className="bg-theme-bg-primary rounded-lg p-4">
<p className="text-theme-text-secondary text-sm text-left">
<span className="font-bold">Command:</span> {config.command}
</p>
<p className="text-theme-text-secondary text-sm text-left">
<span className="font-bold">Arguments:</span>{" "}
{config.args ? config.args.join(" ") : "None"}
</p>
</div>
</div>
);
}
function RenderServerStatus({ server }) {
if (server.running || !server.error) return null;
return (
<div className="flex flex-col gap-y-2">
<p className="text-theme-text-primary text-sm">
This MCP server is not running - it may be stopped or experiencing an
error on startup.
</p>
<div className="bg-theme-bg-primary rounded-lg p-4">
<p className="text-red-500 text-sm font-mono">{server.error}</p>
</div>
</div>
);
}
function RenderServerTools({ tools = [] }) {
if (tools.length === 0) return null;
return (
<div className="flex flex-col gap-y-2">
<div className="flex flex-col gap-y-2">
{tools.map((tool) => (
<ServerTool key={tool.name} tool={tool} />
))}
</div>
</div>
);
}
function ServerTool({ tool }) {
const [open, setOpen] = useState(false);
return (
<button
type="button"
onClick={() => setOpen(!open)}
className="flex flex-col gap-y-2 px-4 py-2 rounded-lg border border-theme-text-secondary"
>
<div className="flex items-center justify-between">
<div className="flex items-center gap-x-2">
<p className="text-theme-text-primary font-mono font-bold text-sm">
{tool.name}
</p>
{!open && (
<p className="text-theme-text-secondary text-sm">
{truncate(tool.description, 70)}
</p>
)}
</div>
<div className="border-none text-theme-text-secondary hover:text-cta-button">
<CaretDown size={16} />
</div>
</div>
{open && (
<div className="flex flex-col gap-y-2">
<div className="flex flex-col gap-y-2">
<p className="text-theme-text-secondary text-sm text-left">
{tool.description}
</p>
</div>
<div className="flex flex-col gap-y-2">
<p className="text-theme-text-primary text-sm text-left">
Tool call arguments
</p>
<div className="flex flex-col gap-y-2">
{Object.entries(tool.inputSchema?.properties || {}).map(
([key, value]) => (
<div key={key} className="flex items-center gap-x-2">
<p className="text-theme-text-secondary text-sm text-left font-bold">
{key}
{tool.inputSchema?.required?.includes(key) && (
<sup className="text-red-500">*</sup>
)}
</p>
<p className="text-theme-text-secondary text-sm text-left">
{value.type}
</p>
</div>
)
)}
</div>
</div>
</div>
)}
</button>
);
}

View File

@ -0,0 +1,147 @@
import { useState, useEffect } from "react";
import { titleCase } from "text-case";
import { Link } from "react-router-dom";
import { BookOpenText, ArrowClockwise } from "@phosphor-icons/react";
import MCPLogo from "@/media/agents/mcp-logo.svg";
import MCPServers from "@/models/mcpServers";
export function MCPServerHeader({ setMcpServers, children }) {
const [loadingMcpServers, setLoadingMcpServers] = useState(false);
useEffect(() => {
async function fetchMCPServers() {
setLoadingMcpServers(true);
const { servers = [] } = await MCPServers.listServers();
setMcpServers(servers);
setLoadingMcpServers(false);
}
fetchMCPServers();
}, []);
// Refresh the list of MCP servers
const refreshMCPServers = () => {
if (
window.confirm(
"Are you sure you want to refresh the list of MCP servers? This will restart all MCP servers and reload their tools."
)
) {
setLoadingMcpServers(true);
MCPServers.forceReload()
.then(({ servers = [] }) => {
setSelectedMcpServer(null);
setMcpServers(servers);
})
.catch((err) => {
console.error(err);
showToast(`Failed to refresh MCP servers.`, "error", { clear: true });
})
.finally(() => {
setLoadingMcpServers(false);
});
}
};
return (
<>
<div className="text-theme-text-primary flex items-center justify-between gap-x-2 mt-4">
<div className="flex items-center gap-x-2">
<img src={MCPLogo} className="w-6 h-6 light:invert" />
<p className="text-lg font-medium">MCP Servers</p>
</div>
<div className="flex items-center gap-x-3">
<Link
to="#goes-to-docs"
target="_blank"
className="border-none text-theme-text-secondary hover:text-cta-button"
>
<BookOpenText size={16} />
</Link>
<button
type="button"
onClick={refreshMCPServers}
disabled={loadingMcpServers}
className="border-none text-theme-text-secondary hover:text-cta-button flex items-center gap-x-1"
>
<ArrowClockwise
size={16}
className={loadingMcpServers ? "animate-spin" : ""}
/>
<p className="text-sm">
{loadingMcpServers ? "Loading..." : "Refresh"}
</p>
</button>
</div>
</div>
{children({ loadingMcpServers })}
</>
);
}
export function MCPServersList({
isLoading = false,
servers = [],
selectedServer,
handleClick,
}) {
if (isLoading) {
return (
<div className="text-theme-text-secondary text-center text-xs flex flex-col gap-y-2">
<p>Loading MCP Servers from configuration file...</p>
<a
href="https://docs.anythingllm.com/mcp-servers/getting-started"
target="_blank"
className="text-theme-text-secondary underline hover:text-cta-button"
>
Learn more about MCP Servers.
</a>
</div>
);
}
if (servers.length === 0) {
return (
<div className="text-theme-text-secondary text-center text-xs flex flex-col gap-y-2">
<p>No MCP servers found</p>
<a
href="https://docs.anythingllm.com/mcp-servers/getting-started"
target="_blank"
className="text-theme-text-secondary underline hover:text-cta-button"
>
Learn more about MCP Servers.
</a>
</div>
);
}
return (
<div className="bg-theme-bg-secondary text-white rounded-xl w-full md:min-w-[360px]">
{servers.map((server, index) => (
<div
key={server.name}
className={`py-3 px-4 flex items-center justify-between ${
index === 0 ? "rounded-t-xl" : ""
} ${
index === servers.length - 1
? "rounded-b-xl"
: "border-b border-white/10"
} cursor-pointer transition-all duration-300 hover:bg-theme-bg-primary ${
selectedServer?.name === server.name
? "bg-white/10 light:bg-theme-bg-sidebar"
: ""
}`}
onClick={() => handleClick?.(server)}
>
<div className="text-sm font-light">
{titleCase(server.name.replace(/[_-]/g, " "))}
</div>
<div className="flex items-center gap-x-2">
<div
className={`text-sm text-theme-text-secondary font-medium ${server.running ? "text-green-500" : "text-red-500"}`}
>
{server.running ? "On" : "Stopped"}
</div>
</div>
</div>
))}
</div>
);
}

View File

@ -11,7 +11,6 @@ import {
Robot,
Hammer,
FlowArrow,
PlusCircle,
} from "@phosphor-icons/react";
import ContextualSaveBar from "@/components/ContextualSaveBar";
import { castToType } from "@/utils/types";
@ -23,6 +22,8 @@ import ImportedSkillConfig from "./Imported/ImportedSkillConfig";
import { Tooltip } from "react-tooltip";
import AgentFlowsList from "./AgentFlows";
import FlowPanel from "./AgentFlows/FlowPanel";
import { MCPServersList, MCPServerHeader } from "./MCPServers";
import ServerPanel from "./MCPServers/ServerPanel";
import { Link } from "react-router-dom";
import paths from "@/utils/paths";
import AgentFlows from "@/models/agentFlows";
@ -43,6 +44,10 @@ export default function AdminAgents() {
const [selectedFlow, setSelectedFlow] = useState(null);
const [activeFlowIds, setActiveFlowIds] = useState([]);
// MCP Servers are lazy loaded to not block the UI thread
const [mcpServers, setMcpServers] = useState([]);
const [selectedMcpServer, setSelectedMcpServer] = useState(null);
// Alert user if they try to leave the page with unsaved changes
useEffect(() => {
const handleBeforeUnload = (event) => {
@ -67,6 +72,7 @@ export default function AdminAgents() {
"active_agent_flows",
]);
const { flows = [] } = await AgentFlows.listFlows();
setSettings({ ..._settings, preferences: _preferences.settings } ?? {});
setAgentSkills(_preferences.settings?.default_agent_skills ?? []);
setDisabledAgentSkills(
@ -109,6 +115,15 @@ export default function AdminAgents() {
});
};
const toggleMCP = (serverName) => {
setMcpServers((prev) => {
return prev.map((server) => {
if (server.name !== serverName) return server;
return { ...server, running: !server.running };
});
});
};
const handleSubmit = async (e) => {
e.preventDefault();
const data = {
@ -159,23 +174,46 @@ export default function AdminAgents() {
setHasChanges(false);
};
const SelectedSkillComponent = selectedFlow
? FlowPanel
: selectedSkill?.imported
? ImportedSkillConfig
: configurableSkills[selectedSkill]?.component ||
defaultSkills[selectedSkill]?.component;
let SelectedSkillComponent = null;
if (selectedFlow) {
SelectedSkillComponent = FlowPanel;
} else if (selectedMcpServer) {
SelectedSkillComponent = ServerPanel;
} else if (selectedSkill?.imported) {
SelectedSkillComponent = ImportedSkillConfig;
} else if (configurableSkills[selectedSkill]) {
SelectedSkillComponent = configurableSkills[selectedSkill]?.component;
} else {
SelectedSkillComponent = defaultSkills[selectedSkill]?.component;
}
// Update the click handlers to clear the other selection
const handleDefaultSkillClick = (skill) => {
setSelectedFlow(null);
setSelectedMcpServer(null);
setSelectedSkill(skill);
if (isMobile) setShowSkillModal(true);
};
const handleSkillClick = (skill) => {
setSelectedFlow(null);
setSelectedMcpServer(null);
setSelectedSkill(skill);
if (isMobile) setShowSkillModal(true);
};
const handleFlowClick = (flow) => {
setSelectedSkill(null);
setSelectedMcpServer(null);
setSelectedFlow(flow);
if (isMobile) setShowSkillModal(true);
};
const handleMCPClick = (server) => {
setSelectedSkill(null);
setSelectedFlow(null);
setSelectedMcpServer(server);
if (isMobile) setShowSkillModal(true);
};
const handleFlowDelete = (flowId) => {
@ -184,6 +222,13 @@ export default function AdminAgents() {
setAgentFlows((prev) => prev.filter((flow) => flow.uuid !== flowId));
};
const handleMCPServerDelete = (serverName) => {
setSelectedMcpServer(null);
setMcpServers((prev) =>
prev.filter((server) => server.name !== serverName)
);
};
if (loading) {
return (
<div
@ -220,7 +265,10 @@ export default function AdminAgents() {
/>
{/* Skill settings nav */}
<div hidden={showSkillModal} className="flex flex-col gap-y-[18px]">
<div
hidden={showSkillModal}
className="flex flex-col gap-y-[18px] overflow-y-scroll no-scroll"
>
<div className="text-theme-text-primary flex items-center gap-x-2">
<Robot size={24} />
<p className="text-lg font-medium">Agent Skills</p>
@ -229,11 +277,7 @@ export default function AdminAgents() {
<SkillList
skills={defaultSkills}
selectedSkill={selectedSkill}
handleClick={(skill) => {
setSelectedFlow(null);
setSelectedSkill(skill);
setShowSkillModal(true);
}}
handleClick={handleDefaultSkillClick}
activeSkills={Object.keys(defaultSkills).filter(
(skill) => !disabledAgentSkills.includes(skill)
)}
@ -242,11 +286,7 @@ export default function AdminAgents() {
<SkillList
skills={configurableSkills}
selectedSkill={selectedSkill}
handleClick={(skill) => {
setSelectedFlow(null);
setSelectedSkill(skill);
setShowSkillModal(true);
}}
handleClick={handleDefaultSkillClick}
activeSkills={agentSkills}
/>
@ -275,6 +315,18 @@ export default function AdminAgents() {
id="active_agent_flows"
value={activeFlowIds.join(",")}
/>
<MCPServerHeader setMcpServers={setMcpServers}>
{({ loadingMcpServers }) => {
return (
<MCPServersList
isLoading={loadingMcpServers}
servers={mcpServers}
selectedServer={selectedMcpServer}
handleClick={handleMCPClick}
/>
);
}}
</MCPServerHeader>
</div>
{/* Selected agent skill modal */}
@ -297,10 +349,16 @@ export default function AdminAgents() {
</button>
</div>
<div className="flex-1 overflow-y-auto p-4">
<div className=" bg-theme-bg-secondary text-white rounded-xl p-4">
<div className=" bg-theme-bg-secondary text-white rounded-xl p-4 overflow-y-scroll no-scroll">
{SelectedSkillComponent ? (
<>
{selectedFlow ? (
{selectedMcpServer ? (
<ServerPanel
server={selectedMcpServer}
toggleServer={toggleMCP}
onDelete={handleMCPServerDelete}
/>
) : selectedFlow ? (
<FlowPanel
flow={selectedFlow}
toggleFlow={toggleFlow}
@ -349,7 +407,7 @@ export default function AdminAgents() {
<div className="flex flex-col items-center justify-center h-full text-theme-text-secondary">
<Robot size={40} />
<p className="font-medium">
Select an agent skill or flow
Select an Agent Skill, Agent Flow, or MCP Server
</p>
</div>
)}
@ -460,16 +518,35 @@ export default function AdminAgents() {
selectedFlow={selectedFlow}
handleClick={handleFlowClick}
/>
<MCPServerHeader setMcpServers={setMcpServers}>
{({ loadingMcpServers }) => {
return (
<MCPServersList
isLoading={loadingMcpServers}
servers={mcpServers}
selectedServer={selectedMcpServer}
handleClick={handleMCPClick}
/>
);
}}
</MCPServerHeader>
</div>
</div>
</div>
{/* Selected agent skill setting panel */}
<div className="flex-[2] flex flex-col gap-y-[18px] mt-10">
<div className="bg-theme-bg-secondary text-white rounded-xl flex-1 p-4">
<div className="bg-theme-bg-secondary text-white rounded-xl flex-1 p-4 overflow-y-scroll no-scroll">
{SelectedSkillComponent ? (
<>
{selectedFlow ? (
{selectedMcpServer ? (
<ServerPanel
server={selectedMcpServer}
toggleServer={toggleMCP}
onDelete={handleMCPServerDelete}
/>
) : selectedFlow ? (
<FlowPanel
flow={selectedFlow}
toggleFlow={toggleFlow}
@ -517,7 +594,9 @@ export default function AdminAgents() {
) : (
<div className="flex flex-col items-center justify-center h-full text-theme-text-secondary">
<Robot size={40} />
<p className="font-medium">Select an agent skill or flow</p>
<p className="font-medium">
Select an Agent Skill, Agent Flow, or MCP Server
</p>
</div>
)}
</div>

1
server/.gitignore vendored
View File

@ -10,6 +10,7 @@ storage/exports
storage/imports
storage/plugins/agent-skills/*
storage/plugins/agent-flows/*
storage/plugins/anythingllm_mcp_servers.json
!storage/documents/DOCUMENTS.md
logs/server.log
*.db

View File

@ -0,0 +1,100 @@
const { reqBody } = require("../utils/http");
const MCPCompatibilityLayer = require("../utils/MCP");
const {
flexUserRoleValid,
ROLES,
} = require("../utils/middleware/multiUserProtected");
const { validatedRequest } = require("../utils/middleware/validatedRequest");
function mcpServersEndpoints(app) {
if (!app) return;
app.get(
"/mcp-servers/force-reload",
[validatedRequest, flexUserRoleValid([ROLES.admin])],
async (_request, response) => {
try {
const mcp = new MCPCompatibilityLayer();
await mcp.reloadMCPServers();
return response.status(200).json({
success: true,
error: null,
servers: await mcp.servers(),
});
} catch (error) {
console.error("Error force reloading MCP servers:", error);
return response.status(500).json({
success: false,
error: error.message,
servers: [],
});
}
}
);
app.get(
"/mcp-servers/list",
[validatedRequest, flexUserRoleValid([ROLES.admin])],
async (_request, response) => {
try {
const servers = await new MCPCompatibilityLayer().servers();
return response.status(200).json({
success: true,
servers,
});
} catch (error) {
console.error("Error listing MCP servers:", error);
return response.status(500).json({
success: false,
error: error.message,
});
}
}
);
app.post(
"/mcp-servers/toggle",
[validatedRequest, flexUserRoleValid([ROLES.admin])],
async (request, response) => {
try {
const { name } = reqBody(request);
const result = await new MCPCompatibilityLayer().toggleServerStatus(
name
);
return response.status(200).json({
success: result.success,
error: result.error,
});
} catch (error) {
console.error("Error toggling MCP server:", error);
return response.status(500).json({
success: false,
error: error.message,
});
}
}
);
app.post(
"/mcp-servers/delete",
[validatedRequest, flexUserRoleValid([ROLES.admin])],
async (request, response) => {
try {
const { name } = reqBody(request);
const result = await new MCPCompatibilityLayer().deleteServer(name);
return response.status(200).json({
success: result.success,
error: result.error,
});
} catch (error) {
console.error("Error deleting MCP server:", error);
return response.status(500).json({
success: false,
error: error.message,
});
}
}
);
}
module.exports = { mcpServersEndpoints };

View File

@ -27,6 +27,7 @@ const { experimentalEndpoints } = require("./endpoints/experimental");
const { browserExtensionEndpoints } = require("./endpoints/browserExtension");
const { communityHubEndpoints } = require("./endpoints/communityHub");
const { agentFlowEndpoints } = require("./endpoints/agentFlows");
const { mcpServersEndpoints } = require("./endpoints/mcpServers");
const app = express();
const apiRouter = express.Router();
const FILE_LIMIT = "3GB";
@ -63,6 +64,7 @@ experimentalEndpoints(apiRouter);
developerEndpoints(app, apiRouter);
communityHubEndpoints(apiRouter);
agentFlowEndpoints(apiRouter);
mcpServersEndpoints(apiRouter);
// Externally facing embedder endpoints
embeddedEndpoints(apiRouter);

View File

@ -34,6 +34,7 @@
"@langchain/textsplitters": "0.0.0",
"@mintplex-labs/bree": "^9.2.5",
"@mintplex-labs/express-ws": "^5.0.7",
"@modelcontextprotocol/sdk": "^1.8.0",
"@pinecone-database/pinecone": "^2.0.1",
"@prisma/client": "5.3.1",
"@qdrant/js-client-rest": "^1.9.0",

View File

@ -0,0 +1,322 @@
const { safeJsonParse } = require("../../http");
const path = require("path");
const fs = require("fs");
const { Client } = require("@modelcontextprotocol/sdk/client/index.js");
const {
StdioClientTransport,
} = require("@modelcontextprotocol/sdk/client/stdio.js");
/**
* @class MCPHypervisor
* @description A class that manages MCP servers found in the storage/plugins/anythingllm_mcp_servers.json file.
* This class is responsible for booting, stopping, and reloading MCP servers - it is the user responsiblity for the MCP server definitions
* to me correct and also functioning tools depending on their deployment (docker vs local) as well as the security of said tools
* since MCP is basically arbitrary code execution.
*
* @notice This class is a singleton.
* @notice Each MCP tool has dependencies specific to it and this call WILL NOT check for them.
* For example, if the tools requires `npx` then the context in which AnythingLLM mains process is running will need to access npx.
* This is typically not common in our pre-built image so may not function. But this is the case anywhere MCP is used.
*
* AnythingLLM will take care of porting MCP servers to agent-callable functions via @agent directive.
* @see MCPCompatibilityLayer.convertServerToolsToPlugins
*/
class MCPHypervisor {
static _instance;
/**
* The path to the JSON file containing the MCP server definitions.
* @type {string}
*/
mcpServerJSONPath;
/**
* The MCP servers currently running.
* @type { { [key: string]: Client & {transport: {_process: import('child_process').ChildProcess}, aibitatToolIds: string[]} } }
*/
mcps = {};
/**
* The results of the MCP server loading process.
* @type { { [key: string]: {status: 'success' | 'failed', message: string} } }
*/
mcpLoadingResults = {};
constructor() {
if (MCPHypervisor._instance) return MCPHypervisor._instance;
MCPHypervisor._instance = this;
this.log("Initializing MCP Hypervisor - subsequent calls will boot faster");
this.#setupConfigFile();
return this;
}
/**
* Setup the MCP server definitions file.
* Will create the file/directory if it doesn't exist already in storage/plugins with blank options
*/
#setupConfigFile() {
this.mcpServerJSONPath =
process.env.NODE_ENV === "development"
? path.resolve(
__dirname,
`../../../storage/plugins/anythingllm_mcp_servers.json`
)
: path.resolve(
process.env.STORAGE_DIR ??
path.resolve(__dirname, `../../../storage`),
`plugins/anythingllm_mcp_servers.json`
);
if (!fs.existsSync(this.mcpServerJSONPath)) {
fs.mkdirSync(path.dirname(this.mcpServerJSONPath), { recursive: true });
fs.writeFileSync(
this.mcpServerJSONPath,
JSON.stringify({ mcpServers: {} }, null, 2),
{ encoding: "utf8" }
);
}
this.log(`MCP Config File: ${this.mcpServerJSONPath}`);
}
log(text, ...args) {
console.log(`\x1b[36m[${this.constructor.name}]\x1b[0m ${text}`, ...args);
}
/**
* Get the MCP servers from the JSON file.
* @returns { { name: string, server: { command: string, args: string[], env: { [key: string]: string } } }[] } The MCP servers.
*/
get mcpServerConfigs() {
const servers = safeJsonParse(
fs.readFileSync(this.mcpServerJSONPath, "utf8"),
{ mcpServers: {} }
);
return Object.entries(servers.mcpServers).map(([name, server]) => ({
name,
server,
}));
}
/**
* Remove the MCP server from the config file
* @param {string} name - The name of the MCP server to remove
* @returns {boolean} - True if the MCP server was removed, false otherwise
*/
removeMCPServerFromConfig(name) {
const servers = safeJsonParse(
fs.readFileSync(this.mcpServerJSONPath, "utf8"),
{ mcpServers: {} }
);
if (!servers.mcpServers[name]) return false;
delete servers.mcpServers[name];
fs.writeFileSync(
this.mcpServerJSONPath,
JSON.stringify(servers, null, 2),
"utf8"
);
this.log(`MCP server ${name} removed from config file`);
return true;
}
/**
* Reload the MCP servers - can be used to reload the MCP servers without restarting the server or app
* and will also apply changes to the config file if any where made.
*/
async reloadMCPServers() {
this.pruneMCPServers();
await this.bootMCPServers();
}
/**
* Start a single MCP server by its server name - public method
* @param {string} name - The name of the MCP server to start
* @returns {Promise<{success: boolean, error: string | null}>}
*/
async startMCPServer(name) {
if (this.mcps[name])
return { success: false, error: `MCP server ${name} already running` };
const config = this.mcpServerConfigs.find((s) => s.name === name);
if (!config)
return {
success: false,
error: `MCP server ${name} not found in config file`,
};
try {
await this.#startMCPServer(config);
this.mcpLoadingResults[name] = {
status: "success",
message: `Successfully connected to MCP server: ${name}`,
};
return { success: true, message: `MCP server ${name} started` };
} catch (e) {
this.log(`Failed to start single MCP server: ${name}`, {
error: e.message,
code: e.code,
syscall: e.syscall,
path: e.path,
stack: e.stack,
});
this.mcpLoadingResults[name] = {
status: "failed",
message: `Failed to start MCP server: ${name} [${e.code || "NO_CODE"}] ${e.message}`,
};
// Clean up failed connection
if (this.mcps[name]) {
this.mcps[name].close();
delete this.mcps[name];
}
return { success: false, error: e.message };
}
}
/**
* Prune a single MCP server by its server name
* @param {string} name - The name of the MCP server to prune
* @returns {boolean} - True if the MCP server was pruned, false otherwise
*/
pruneMCPServer(name) {
if (!name || !this.mcps[name]) return true;
this.log(`Pruning MCP server: ${name}`);
const mcp = this.mcps[name];
const childProcess = mcp.transport._process;
if (childProcess) childProcess.kill(1);
mcp.transport.close();
delete this.mcps[name];
this.mcpLoadingResults[name] = {
status: "failed",
message: `Server was stopped manually by the administrator.`,
};
return true;
}
/**
* Prune the MCP servers - pkills and forgets all MCP servers
* @returns {void}
*/
pruneMCPServers() {
this.log(`Pruning ${Object.keys(this.mcps).length} MCP servers...`);
for (const name of Object.keys(this.mcps)) {
if (!this.mcps[name]) continue;
const mcp = this.mcps[name];
const childProcess = mcp.transport._process;
if (childProcess)
this.log(`Killing MCP ${name} (PID: ${childProcess.pid})`, {
killed: childProcess.kill(1),
});
mcp.transport.close();
mcp.close();
}
this.mcps = {};
this.mcpLoadingResults = {};
}
/**
* @private Start a single MCP server by its server definition from the JSON file
* @param {string} name - The name of the MCP server to start
* @param {Object} server - The server definition
* @returns {Promise<boolean>}
*/
async #startMCPServer({ name, server }) {
if (!name) throw new Error("MCP server name is required");
if (!server) throw new Error("MCP server definition is required");
if (!server.command) throw new Error("MCP server command is required");
if (server.hasOwnProperty("args") && !Array.isArray(server.args))
throw new Error("MCP server args must be an array");
this.log(`Attempting to start MCP server: ${name}`);
const mcp = new Client({ name: name, version: "1.0.0" });
const transport = new StdioClientTransport({
command: server.command,
args: server?.args ?? [],
});
// Add connection event listeners
transport.onclose = () => this.log(`${name} - Transport closed`);
transport.onerror = (error) =>
this.log(`${name} - Transport error:`, error);
transport.onmessage = (message) =>
this.log(`${name} - Transport message:`, message);
// Connect and await the connection with a timeout
this.mcps[name] = mcp;
const connectionPromise = mcp.connect(transport);
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => reject(new Error("Connection timeout")), 30_000); // 30 second timeout
});
await Promise.race([connectionPromise, timeoutPromise]);
return true;
}
/**
* Boot the MCP servers according to the server definitions.
* This function will skip booting MCP servers if they are already running.
* @returns { Promise<{ [key: string]: {status: string, message: string} }> } The results of the boot process.
*/
async bootMCPServers() {
if (Object.keys(this.mcps).length > 0) {
this.log("MCP Servers already running, skipping boot.");
return this.mcpLoadingResults;
}
const serverDefinitions = this.mcpServerConfigs;
for (const { name, server } of serverDefinitions) {
if (
server.anythingllm?.hasOwnProperty("autoStart") &&
server.anythingllm.autoStart === false
) {
this.log(
`MCP server ${name} has anythingllm.autoStart property set to false, skipping boot!`
);
this.mcpLoadingResults[name] = {
status: "failed",
message: `MCP server ${name} has anythingllm.autoStart property set to false, boot skipped!`,
};
continue;
}
try {
await this.#startMCPServer({ name, server });
// Verify the connection is alive?
// if (!(await mcp.ping())) throw new Error('Connection failed to establish');
this.mcpLoadingResults[name] = {
status: "success",
message: `Successfully connected to MCP server: ${name}`,
};
} catch (e) {
this.log(`Failed to start MCP server: ${name}`, {
error: e.message,
code: e.code,
syscall: e.syscall,
path: e.path,
stack: e.stack, // Adding stack trace for better debugging
});
this.mcpLoadingResults[name] = {
status: "failed",
message: `Failed to start MCP server: ${name} [${e.code || "NO_CODE"}] ${e.message}`,
};
// Clean up failed connection
if (this.mcps[name]) {
this.mcps[name].close();
delete this.mcps[name];
}
}
}
const runningServers = Object.keys(this.mcps);
this.log(
`Successfully started ${runningServers.length} MCP servers:`,
runningServers
);
return this.mcpLoadingResults;
}
}
module.exports = MCPHypervisor;

203
server/utils/MCP/index.js Normal file
View File

@ -0,0 +1,203 @@
const MCPHypervisor = require("./hypervisor");
class MCPCompatibilityLayer extends MCPHypervisor {
static _instance;
constructor() {
super();
if (MCPCompatibilityLayer._instance) return MCPCompatibilityLayer._instance;
MCPCompatibilityLayer._instance = this;
}
/**
* Get all of the active MCP servers as plugins we can load into agents.
* This will also boot all MCP servers if they have not been started yet.
* @returns {Promise<string[]>} Array of flow names in @@mcp_{name} format
*/
async activeMCPServers() {
await this.bootMCPServers();
return Object.keys(this.mcps).flatMap((name) => `@@mcp_${name}`);
}
/**
* Convert an MCP server name to an AnythingLLM Agent plugin
* @param {string} name - The base name of the MCP server to convert - not the tool name. eg: `docker-mcp` not `docker-mcp:list-containers`
* @param {Object} aibitat - The aibitat object to pass to the plugin
* @returns {Promise<{name: string, description: string, plugin: Function}[]|null>} Array of plugin configurations or null if not found
*/
async convertServerToolsToPlugins(name, _aibitat = null) {
const mcp = this.mcps[name];
if (!mcp) return null;
const tools = (await mcp.listTools()).tools;
if (!tools.length) return null;
const plugins = [];
for (const tool of tools) {
plugins.push({
name: `${name}-${tool.name}`,
description: tool.description,
plugin: function () {
return {
name: `${name}-${tool.name}`,
setup: (aibitat) => {
aibitat.function({
super: aibitat,
name: `${name}-${tool.name}`,
controller: new AbortController(),
description: tool.description,
examples: [],
parameters: {
$schema: "http://json-schema.org/draft-07/schema#",
...tool.inputSchema,
},
handler: async function (args = {}) {
try {
aibitat.handlerProps.log(
`Executing MCP server: ${name}:${tool.name} with args:`,
args
);
aibitat.introspect(
`Executing MCP server: ${name} with ${JSON.stringify(args, null, 2)}`
);
const result = await mcp.callTool({
name: tool.name,
arguments: args,
});
aibitat.handlerProps.log(
`MCP server: ${name}:${tool.name} completed successfully`,
result
);
aibitat.introspect(
`MCP server: ${name}:${tool.name} completed successfully`
);
return typeof result === "object"
? JSON.stringify(result)
: String(result);
} catch (error) {
aibitat.handlerProps.log(
`MCP server: ${name}:${tool.name} failed with error:`,
error
);
aibitat.introspect(
`MCP server: ${name}:${tool.name} failed with error:`,
error
);
return `The tool ${name}:${tool.name} failed with error: ${error?.message || "An unknown error occurred"}`;
}
},
});
},
};
},
toolName: `${name}:${tool.name}`,
});
}
return plugins;
}
/**
* Returns the MCP servers that were loaded or attempted to be loaded
* so that we can display them in the frontend for review or error logging.
* @returns {Promise<{
* name: string,
* running: boolean,
* tools: {name: string, description: string, inputSchema: Object}[],
* process: {pid: number, cmd: string}|null,
* error: string|null
* }[]>} - The active MCP servers
*/
async servers() {
await this.bootMCPServers();
const servers = [];
for (const [name, result] of Object.entries(this.mcpLoadingResults)) {
const config = this.mcpServerConfigs.find((s) => s.name === name);
if (result.status === "failed") {
servers.push({
name,
config: config?.server || null,
running: false,
tools: [],
error: result.message,
process: null,
});
continue;
}
const mcp = this.mcps[name];
if (!mcp) {
delete this.mcpLoadingResults[name];
delete this.mcps[name];
continue;
}
const online = !!(await mcp.ping());
const tools = online ? (await mcp.listTools()).tools : [];
servers.push({
name,
config: config?.server || null,
running: online,
tools,
error: null,
process: {
pid: mcp.transport._process.pid,
},
});
}
return servers;
}
/**
* Toggle the MCP server (start or stop)
* @param {string} name - The name of the MCP server to toggle
* @returns {Promise<{success: boolean, error: string | null}>}
*/
async toggleServerStatus(name) {
const server = this.mcpServerConfigs.find((s) => s.name === name);
if (!server)
return {
success: false,
error: `MCP server ${name} not found in config file.`,
};
const mcp = this.mcps[name];
const online = !!mcp ? !!(await mcp.ping()) : false; // If the server is not in the mcps object, it is not running
if (online) {
const killed = this.pruneMCPServer(name);
return {
success: killed,
error: killed ? null : `Failed to kill MCP server: ${name}`,
};
} else {
const startupResult = await this.startMCPServer(name);
return { success: startupResult.success, error: startupResult.error };
}
}
/**
* Delete the MCP server - will also remove it from the config file
* @param {string} name - The name of the MCP server to delete
* @returns {Promise<{success: boolean, error: string | null}>}
*/
async deleteServer(name) {
const server = this.mcpServerConfigs.find((s) => s.name === name);
if (!server)
return {
success: false,
error: `MCP server ${name} not found in config file.`,
};
const mcp = this.mcps[name];
const online = !!mcp ? !!(await mcp.ping()) : false; // If the server is not in the mcps object, it is not running
if (online) this.pruneMCPServer(name);
this.removeMCPServerFromConfig(name);
delete this.mcps[name];
delete this.mcpLoadingResults[name];
this.log(`MCP server was killed and removed from config file: ${name}`);
return { success: true, error: null };
}
}
module.exports = MCPCompatibilityLayer;

View File

@ -4,6 +4,7 @@ const { safeJsonParse } = require("../http");
const Provider = require("./aibitat/providers/ai-provider");
const ImportedPlugin = require("./imported");
const { AgentFlows } = require("../agentFlows");
const MCPCompatibilityLayer = require("../MCP");
// This is a list of skills that are built-in and default enabled.
const DEFAULT_SKILLS = [
@ -31,6 +32,7 @@ const WORKSPACE_AGENT = {
...(await agentSkillsFromSystemSettings()),
...ImportedPlugin.activeImportedPlugins(),
...AgentFlows.activeFlowPlugins(),
...(await new MCPCompatibilityLayer().activeMCPServers()),
],
};
},

View File

@ -8,6 +8,7 @@ const { safeJsonParse } = require("../http");
const { USER_AGENT, WORKSPACE_AGENT } = require("./defaults");
const ImportedPlugin = require("./imported");
const { AgentFlows } = require("../agentFlows");
const MCPCompatibilityLayer = require("../MCP");
class AgentHandler {
#invocationUUID;
@ -411,6 +412,43 @@ class AgentHandler {
continue;
}
// Load MCP plugin. This is marked by `@@mcp_` in the array of functions to load.
// All sub-tools are loaded here and are denoted by `pluginName:toolName` as their identifier.
// This will replace the parent MCP server plugin with the sub-tools as child plugins so they
// can be called directly by the agent when invoked.
// Since to get to this point, the `activeMCPServers` method has already been called, we can
// safely assume that the MCP server is running and the tools are available/loaded.
if (name.startsWith("@@mcp_")) {
const mcpPluginName = name.replace("@@mcp_", "");
const plugins =
await new MCPCompatibilityLayer().convertServerToolsToPlugins(
mcpPluginName,
this.aibitat
);
if (!plugins) {
this.log(
`MCP ${mcpPluginName} not found in MCP server config. Skipping inclusion to agent cluster.`
);
continue;
}
// Remove the old function from the agent functions directly
// and push the new ones onto the end of the array so that they are loaded properly.
this.aibitat.agents.get("@agent").functions = this.aibitat.agents
.get("@agent")
.functions.filter((f) => f.name !== name);
for (const plugin of plugins)
this.aibitat.agents.get("@agent").functions.push(plugin.name);
plugins.forEach((plugin) => {
this.aibitat.use(plugin.plugin());
this.log(
`Attached MCP::${plugin.toolName} MCP tool to Agent cluster`
);
});
continue;
}
// Load imported plugin. This is marked by `@@` in the array of functions to load.
// and is the @@hubID of the plugin.
if (name.startsWith("@@")) {

View File

@ -1650,6 +1650,22 @@
dependencies:
ws "^7.5.10"
"@modelcontextprotocol/sdk@^1.8.0":
version "1.8.0"
resolved "https://registry.yarnpkg.com/@modelcontextprotocol/sdk/-/sdk-1.8.0.tgz#55cdd65054ec24e53800250c70e07429d669db67"
integrity sha512-e06W7SwrontJDHwCawNO5SGxG+nU9AAx+jpHHZqGl/WrDBdWOpvirC+s58VpJTB5QemI4jTRcjWT4Pt3Q1NPQQ==
dependencies:
content-type "^1.0.5"
cors "^2.8.5"
cross-spawn "^7.0.3"
eventsource "^3.0.2"
express "^5.0.1"
express-rate-limit "^7.5.0"
pkce-challenge "^4.1.0"
raw-body "^3.0.0"
zod "^3.23.8"
zod-to-json-schema "^3.24.1"
"@nodelib/fs.scandir@2.1.5":
version "2.1.5"
resolved "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz"
@ -2790,6 +2806,14 @@ abort-controller@^3.0.0:
dependencies:
event-target-shim "^5.0.0"
accepts@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/accepts/-/accepts-2.0.0.tgz#bbcf4ba5075467f3f2131eab3cffc73c2f5d7895"
integrity sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==
dependencies:
mime-types "^3.0.0"
negotiator "^1.0.0"
accepts@~1.3.8:
version "1.3.8"
resolved "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz"
@ -3170,6 +3194,21 @@ body-parser@1.20.2, body-parser@^1.20.2:
type-is "~1.6.18"
unpipe "1.0.0"
body-parser@^2.0.1:
version "2.1.0"
resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-2.1.0.tgz#2fd84396259e00fa75648835e2d95703bce8e890"
integrity sha512-/hPxh61E+ll0Ujp24Ilm64cykicul1ypfwjVttduAiEdtnJFvLePSrIPk+HMImtNv5270wOGCb1Tns2rybMkoQ==
dependencies:
bytes "^3.1.2"
content-type "^1.0.5"
debug "^4.4.0"
http-errors "^2.0.0"
iconv-lite "^0.5.2"
on-finished "^2.4.1"
qs "^6.14.0"
raw-body "^3.0.0"
type-is "^2.0.0"
boolbase@^1.0.0:
version "1.0.0"
resolved "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz"
@ -3238,11 +3277,19 @@ busboy@^1.0.0:
dependencies:
streamsearch "^1.1.0"
bytes@3.1.2:
bytes@3.1.2, bytes@^3.1.2:
version "3.1.2"
resolved "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz"
integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==
call-bind-apply-helpers@^1.0.1, call-bind-apply-helpers@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz#4b5428c222be985d79c3d82657479dbe0b59b2d6"
integrity sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==
dependencies:
es-errors "^1.3.0"
function-bind "^1.1.2"
call-bind@^1.0.2, call-bind@^1.0.5, call-bind@^1.0.6, call-bind@^1.0.7:
version "1.0.7"
resolved "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz"
@ -3254,6 +3301,14 @@ call-bind@^1.0.2, call-bind@^1.0.5, call-bind@^1.0.6, call-bind@^1.0.7:
get-intrinsic "^1.2.4"
set-function-length "^1.2.1"
call-bound@^1.0.2:
version "1.0.4"
resolved "https://registry.yarnpkg.com/call-bound/-/call-bound-1.0.4.tgz#238de935d2a2a692928c538c7ccfa91067fd062a"
integrity sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==
dependencies:
call-bind-apply-helpers "^1.0.2"
get-intrinsic "^1.3.0"
callsites@^3.0.0:
version "3.1.0"
resolved "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz"
@ -3572,7 +3627,14 @@ content-disposition@0.5.4:
dependencies:
safe-buffer "5.2.1"
content-type@~1.0.4, content-type@~1.0.5:
content-disposition@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-1.0.0.tgz#844426cb398f934caefcbb172200126bc7ceace2"
integrity sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==
dependencies:
safe-buffer "5.2.1"
content-type@^1.0.5, content-type@~1.0.4, content-type@~1.0.5:
version "1.0.5"
resolved "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz"
integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==
@ -3582,11 +3644,21 @@ cookie-signature@1.0.6:
resolved "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz"
integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==
cookie-signature@^1.2.1:
version "1.2.2"
resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.2.2.tgz#57c7fc3cc293acab9fec54d73e15690ebe4a1793"
integrity sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==
cookie@0.6.0:
version "0.6.0"
resolved "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz"
integrity sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==
cookie@0.7.1:
version "0.7.1"
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.7.1.tgz#2f73c42142d5d5cf71310a74fc4ae61670e5dbc9"
integrity sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==
core-util-is@~1.0.0:
version "1.0.3"
resolved "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz"
@ -3711,6 +3783,13 @@ debug@4, debug@^4.3.1, debug@^4.3.2, debug@^4.3.3, debug@^4.3.4:
dependencies:
ms "2.1.2"
debug@4.3.6:
version "4.3.6"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.6.tgz#2ab2c38fbaffebf8aa95fdfe6d88438c7a13c52b"
integrity sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==
dependencies:
ms "2.1.2"
debug@^3.2.7:
version "3.2.7"
resolved "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz"
@ -3718,6 +3797,13 @@ debug@^3.2.7:
dependencies:
ms "^2.1.1"
debug@^4.3.5, debug@^4.4.0:
version "4.4.0"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.0.tgz#2b3f2aea2ffeb776477460267377dc8710faba8a"
integrity sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==
dependencies:
ms "^2.1.3"
decamelize@1.2.0:
version "1.2.0"
resolved "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz"
@ -3788,7 +3874,7 @@ depd@2.0.0:
resolved "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz"
integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==
destroy@1.2.0:
destroy@1.2.0, destroy@^1.2.0:
version "1.2.0"
resolved "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz"
integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==
@ -3864,6 +3950,15 @@ dotenv@^16.0.3:
resolved "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz"
integrity sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==
dunder-proto@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/dunder-proto/-/dunder-proto-1.0.1.tgz#d7ae667e1dc83482f8b70fd0f6eefc50da30f58a"
integrity sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==
dependencies:
call-bind-apply-helpers "^1.0.1"
es-errors "^1.3.0"
gopd "^1.2.0"
ecdsa-sig-formatter@1.0.11:
version "1.0.11"
resolved "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz"
@ -3903,6 +3998,11 @@ encode32@^1.1.0:
resolved "https://registry.npmjs.org/encode32/-/encode32-1.1.0.tgz"
integrity sha512-BCmijZ4lWec5+fuGHclf7HSZf+mos2ncQkBjtvomvRWVEGAMI/tw56fuN2x4e+FTgQuTPbZjODPwX80lFy958w==
encodeurl@^2.0.0, encodeurl@~2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-2.0.0.tgz#7b8ea898077d7e409d3ac45474ea38eaf0857a58"
integrity sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==
encodeurl@~1.0.2:
version "1.0.2"
resolved "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz"
@ -4008,6 +4108,11 @@ es-define-property@^1.0.0:
dependencies:
get-intrinsic "^1.2.4"
es-define-property@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.1.tgz#983eb2f9a6724e9303f61addf011c72e09e0b0fa"
integrity sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==
es-errors@^1.1.0, es-errors@^1.2.1, es-errors@^1.3.0:
version "1.3.0"
resolved "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz"
@ -4040,6 +4145,13 @@ es-object-atoms@^1.0.0:
dependencies:
es-errors "^1.3.0"
es-object-atoms@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz#1c4f2c4837327597ce69d2ca190a7fdd172338c1"
integrity sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==
dependencies:
es-errors "^1.3.0"
es-set-tostringtag@^2.0.3:
version "2.0.3"
resolved "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz"
@ -4070,7 +4182,7 @@ escalade@^3.1.1:
resolved "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz"
integrity sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==
escape-html@~1.0.3:
escape-html@^1.0.3, escape-html@~1.0.3:
version "1.0.3"
resolved "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz"
integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==
@ -4225,7 +4337,7 @@ esutils@^2.0.2:
resolved "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz"
integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==
etag@~1.8.1:
etag@^1.8.1, etag@~1.8.1:
version "1.8.1"
resolved "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz"
integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==
@ -4245,6 +4357,18 @@ events@^3.0.0, events@^3.3.0:
resolved "https://registry.npmjs.org/events/-/events-3.3.0.tgz"
integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==
eventsource-parser@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/eventsource-parser/-/eventsource-parser-3.0.0.tgz#9303e303ef807d279ee210a17ce80f16300d9f57"
integrity sha512-T1C0XCUimhxVQzW4zFipdx0SficT651NnkR0ZSH3yQwh+mFMdLfgjABVi4YtMTtaL4s168593DaoaRLMqryavA==
eventsource@^3.0.2:
version "3.0.5"
resolved "https://registry.yarnpkg.com/eventsource/-/eventsource-3.0.5.tgz#0cae1eee2d2c75894de8b02a91d84e5c57f0cc5a"
integrity sha512-LT/5J605bx5SNyE+ITBDiM3FxffBiq9un7Vx0EwMDM3vg8sWKx/tO2zC+LMqZ+smAM0F2hblaDZUVZF0te2pSw==
dependencies:
eventsource-parser "^3.0.0"
execa@^5.1.1:
version "5.1.1"
resolved "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz"
@ -4270,6 +4394,11 @@ expr-eval@^2.0.2:
resolved "https://registry.npmjs.org/expr-eval/-/expr-eval-2.0.2.tgz"
integrity sha512-4EMSHGOPSwAfBiibw3ndnP0AvjDWLsMvGOvWEZ2F96IGk0bIVdjQisOHxReSkE13mHcfbuCiXw+G4y0zv6N8Eg==
express-rate-limit@^7.5.0:
version "7.5.0"
resolved "https://registry.yarnpkg.com/express-rate-limit/-/express-rate-limit-7.5.0.tgz#6a67990a724b4fbbc69119419feef50c51e8b28f"
integrity sha512-eB5zbQh5h+VenMPM3fh+nw1YExi5nMr6HUCR62ELSP11huvxm/Uir1H1QEyTkk5QX6A58pX6NmaTMceKZ0Eodg==
express@^4.18.2:
version "4.19.2"
resolved "https://registry.npmjs.org/express/-/express-4.19.2.tgz"
@ -4307,6 +4436,44 @@ express@^4.18.2:
utils-merge "1.0.1"
vary "~1.1.2"
express@^5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/express/-/express-5.0.1.tgz#5d359a2550655be33124ecbc7400cd38436457e9"
integrity sha512-ORF7g6qGnD+YtUG9yx4DFoqCShNMmUKiXuT5oWMHiOvt/4WFbHC6yCwQMTSBMno7AqntNCAzzcnnjowRkTL9eQ==
dependencies:
accepts "^2.0.0"
body-parser "^2.0.1"
content-disposition "^1.0.0"
content-type "~1.0.4"
cookie "0.7.1"
cookie-signature "^1.2.1"
debug "4.3.6"
depd "2.0.0"
encodeurl "~2.0.0"
escape-html "~1.0.3"
etag "~1.8.1"
finalhandler "^2.0.0"
fresh "2.0.0"
http-errors "2.0.0"
merge-descriptors "^2.0.0"
methods "~1.1.2"
mime-types "^3.0.0"
on-finished "2.4.1"
once "1.4.0"
parseurl "~1.3.3"
proxy-addr "~2.0.7"
qs "6.13.0"
range-parser "~1.2.1"
router "^2.0.0"
safe-buffer "5.2.1"
send "^1.1.0"
serve-static "^2.1.0"
setprototypeof "1.2.0"
statuses "2.0.1"
type-is "^2.0.0"
utils-merge "1.0.1"
vary "~1.1.2"
external-editor@^3.1.0:
version "3.1.0"
resolved "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz"
@ -4411,6 +4578,18 @@ finalhandler@1.2.0:
statuses "2.0.1"
unpipe "~1.0.0"
finalhandler@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-2.1.0.tgz#72306373aa89d05a8242ed569ed86a1bff7c561f"
integrity sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==
dependencies:
debug "^4.4.0"
encodeurl "^2.0.0"
escape-html "^1.0.3"
on-finished "^2.4.1"
parseurl "^1.3.3"
statuses "^2.0.1"
find-replace@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/find-replace/-/find-replace-3.0.0.tgz#3e7e23d3b05167a76f770c9fbd5258b0def68c38"
@ -4532,11 +4711,16 @@ forwarded@0.2.0:
resolved "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz"
integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==
fresh@0.5.2:
fresh@0.5.2, fresh@^0.5.2:
version "0.5.2"
resolved "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz"
integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==
fresh@2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/fresh/-/fresh-2.0.0.tgz#8dd7df6a1b3a1b3a5cf186c05a5dd267622635a4"
integrity sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==
fs-constants@^1.0.0:
version "1.0.0"
resolved "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz"
@ -4622,6 +4806,30 @@ get-intrinsic@^1.1.3, get-intrinsic@^1.2.1, get-intrinsic@^1.2.3, get-intrinsic@
has-symbols "^1.0.3"
hasown "^2.0.0"
get-intrinsic@^1.2.5, get-intrinsic@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz#743f0e3b6964a93a5491ed1bffaae054d7f98d01"
integrity sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==
dependencies:
call-bind-apply-helpers "^1.0.2"
es-define-property "^1.0.1"
es-errors "^1.3.0"
es-object-atoms "^1.1.1"
function-bind "^1.1.2"
get-proto "^1.0.1"
gopd "^1.2.0"
has-symbols "^1.1.0"
hasown "^2.0.2"
math-intrinsics "^1.1.0"
get-proto@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/get-proto/-/get-proto-1.0.1.tgz#150b3f2743869ef3e851ec0c49d15b1d14d00ee1"
integrity sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==
dependencies:
dunder-proto "^1.0.1"
es-object-atoms "^1.0.0"
get-stream@^6.0.0:
version "6.0.1"
resolved "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz"
@ -4689,6 +4897,11 @@ gopd@^1.0.1:
dependencies:
get-intrinsic "^1.1.3"
gopd@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1"
integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==
graphemer@^1.4.0:
version "1.4.0"
resolved "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz"
@ -4746,6 +4959,11 @@ has-symbols@^1.0.2, has-symbols@^1.0.3:
resolved "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz"
integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==
has-symbols@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.1.0.tgz#fc9c6a783a084951d0b971fe1018de813707a338"
integrity sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==
has-tostringtag@^1.0.0, has-tostringtag@^1.0.2:
version "1.0.2"
resolved "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz"
@ -4813,7 +5031,7 @@ htmlparser2@^9.1.0:
domutils "^3.1.0"
entities "^4.5.0"
http-errors@2.0.0:
http-errors@2.0.0, http-errors@^2.0.0:
version "2.0.0"
resolved "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz"
integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==
@ -4881,6 +5099,13 @@ iconv-lite@0.6.3, iconv-lite@^0.6.2, iconv-lite@^0.6.3:
dependencies:
safer-buffer ">= 2.1.2 < 3.0.0"
iconv-lite@^0.5.2:
version "0.5.2"
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.5.2.tgz#af6d628dccfb463b7364d97f715e4b74b8c8c2b8"
integrity sha512-kERHXvpSaB4aU3eANwidg79K8FlrN77m8G9V+0vOR3HYaRifrlwMEpT7ZBJqLSEIHnEgJTHcWK82wwLwwKwtag==
dependencies:
safer-buffer ">= 2.1.2 < 3"
ieee754@^1.1.13, ieee754@^1.2.1:
version "1.2.1"
resolved "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz"
@ -5101,6 +5326,11 @@ is-path-inside@^3.0.3:
resolved "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz"
integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==
is-promise@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-4.0.0.tgz#42ff9f84206c1991d26debf520dd5c01042dd2f3"
integrity sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==
is-property@^1.0.2:
version "1.0.2"
resolved "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz"
@ -5635,6 +5865,11 @@ make-dir@^3.1.0:
dependencies:
semver "^6.0.0"
math-intrinsics@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz#a0dd74be81e2aa5c2f27e65ce283605ee4e2b7f9"
integrity sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==
md5@^2.3.0:
version "2.3.0"
resolved "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz"
@ -5649,11 +5884,21 @@ media-typer@0.3.0:
resolved "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz"
integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==
media-typer@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-1.1.0.tgz#6ab74b8f2d3320f2064b2a87a38e7931ff3a5561"
integrity sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==
merge-descriptors@1.0.1:
version "1.0.1"
resolved "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz"
integrity sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==
merge-descriptors@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-2.0.0.tgz#ea922f660635a2249ee565e0449f951e6b603808"
integrity sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==
merge-stream@^2.0.0:
version "2.0.0"
resolved "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz"
@ -5669,13 +5914,25 @@ mime-db@1.52.0:
resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz"
integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==
mime-types@^2.1.12, mime-types@~2.1.24, mime-types@~2.1.34:
mime-db@^1.54.0:
version "1.54.0"
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.54.0.tgz#cddb3ee4f9c64530dff640236661d42cb6a314f5"
integrity sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==
mime-types@^2.1.12, mime-types@^2.1.35, mime-types@~2.1.24, mime-types@~2.1.34:
version "2.1.35"
resolved "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz"
integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==
dependencies:
mime-db "1.52.0"
mime-types@^3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-3.0.1.tgz#b1d94d6997a9b32fd69ebaed0db73de8acb519ce"
integrity sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==
dependencies:
mime-db "^1.54.0"
mime@1.6.0:
version "1.6.0"
resolved "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz"
@ -5883,6 +6140,11 @@ negotiator@0.6.3:
resolved "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz"
integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==
negotiator@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-1.0.0.tgz#b6c91bb47172d69f93cfd7c357bbb529019b5f6a"
integrity sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==
node-abi@^3.3.0:
version "3.62.0"
resolved "https://registry.npmjs.org/node-abi/-/node-abi-3.62.0.tgz"
@ -6016,6 +6278,11 @@ object-inspect@^1.13.1:
resolved "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz"
integrity sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==
object-inspect@^1.13.3:
version "1.13.4"
resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.4.tgz#8375265e21bc20d0fa582c22e1b13485d6e00213"
integrity sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==
object-keys@^1.1.1:
version "1.1.1"
resolved "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz"
@ -6075,14 +6342,14 @@ ollama@^0.5.0, ollama@^0.5.10:
dependencies:
whatwg-fetch "^3.6.20"
on-finished@2.4.1:
on-finished@2.4.1, on-finished@^2.4.1:
version "2.4.1"
resolved "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz"
integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==
dependencies:
ee-first "1.1.1"
once@^1.3.0, once@^1.3.1, once@^1.4.0:
once@1.4.0, once@^1.3.0, once@^1.3.1, once@^1.4.0:
version "1.4.0"
resolved "https://registry.npmjs.org/once/-/once-1.4.0.tgz"
integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==
@ -6276,7 +6543,7 @@ parse5@^7.0.0, parse5@^7.1.2:
dependencies:
entities "^4.5.0"
parseurl@~1.3.3:
parseurl@^1.3.3, parseurl@~1.3.3:
version "1.3.3"
resolved "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz"
integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==
@ -6306,6 +6573,11 @@ path-to-regexp@0.1.7:
resolved "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz"
integrity sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==
path-to-regexp@^8.0.0:
version "8.2.0"
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-8.2.0.tgz#73990cc29e57a3ff2a0d914095156df5db79e8b4"
integrity sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==
pg-cloudflare@^1.1.1:
version "1.1.1"
resolved "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.1.1.tgz"
@ -6381,6 +6653,11 @@ pirates@^3.0.2:
dependencies:
node-modules-regexp "^1.0.0"
pkce-challenge@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/pkce-challenge/-/pkce-challenge-4.1.0.tgz#95027d7750c3c0f21676a345b48f481786f9acdb"
integrity sha512-ZBmhE1C9LcPoH9XZSdwiPtbPHZROwAnMy+kIFQVrnMCxY4Cudlz3gBOpzilgc0jOgRaiT3sIWfpMomW2ar2orQ==
platform@^1.3.6:
version "1.3.6"
resolved "https://registry.npmjs.org/platform/-/platform-1.3.6.tgz"
@ -6574,6 +6851,20 @@ qs@6.11.2:
dependencies:
side-channel "^1.0.4"
qs@6.13.0:
version "6.13.0"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.13.0.tgz#6ca3bd58439f7e245655798997787b0d88a51906"
integrity sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==
dependencies:
side-channel "^1.0.6"
qs@^6.14.0:
version "6.14.0"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.14.0.tgz#c63fa40680d2c5c941412a0e899c89af60c0a930"
integrity sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==
dependencies:
side-channel "^1.1.0"
queue-microtask@^1.2.2:
version "1.2.3"
resolved "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz"
@ -6584,7 +6875,7 @@ queue-tick@^1.0.1:
resolved "https://registry.npmjs.org/queue-tick/-/queue-tick-1.0.1.tgz"
integrity sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==
range-parser@~1.2.1:
range-parser@^1.2.1, range-parser@~1.2.1:
version "1.2.1"
resolved "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz"
integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==
@ -6599,6 +6890,16 @@ raw-body@2.5.2:
iconv-lite "0.4.24"
unpipe "1.0.0"
raw-body@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-3.0.0.tgz#25b3476f07a51600619dae3fe82ddc28a36e5e0f"
integrity sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==
dependencies:
bytes "3.1.2"
http-errors "2.0.0"
iconv-lite "0.6.3"
unpipe "1.0.0"
rc@^1.2.7:
version "1.2.8"
resolved "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz"
@ -6744,6 +7045,15 @@ rimraf@^3.0.2:
dependencies:
glob "^7.1.3"
router@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/router/-/router-2.1.0.tgz#f256ca2365afb4d386ba4f7a9ee0aa0827c962fa"
integrity sha512-/m/NSLxeYEgWNtyC+WtNHCF7jbGxOibVWKnn+1Psff4dJGOfoXP+MuC/f2CwSmyiHdOIzYnYFp4W6GxWfekaLA==
dependencies:
is-promise "^4.0.0"
parseurl "^1.3.3"
path-to-regexp "^8.0.0"
run-parallel@^1.1.9:
version "1.2.0"
resolved "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz"
@ -6841,6 +7151,24 @@ send@0.18.0:
range-parser "~1.2.1"
statuses "2.0.1"
send@^1.0.0, send@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/send/-/send-1.1.0.tgz#4efe6ff3bb2139b0e5b2648d8b18d4dec48fc9c5"
integrity sha512-v67WcEouB5GxbTWL/4NeToqcZiAWEq90N888fczVArY8A79J0L4FD7vj5hm3eUMua5EpoQ59wa/oovY6TLvRUA==
dependencies:
debug "^4.3.5"
destroy "^1.2.0"
encodeurl "^2.0.0"
escape-html "^1.0.3"
etag "^1.8.1"
fresh "^0.5.2"
http-errors "^2.0.0"
mime-types "^2.1.35"
ms "^2.1.3"
on-finished "^2.4.1"
range-parser "^1.2.1"
statuses "^2.0.1"
seq-queue@^0.0.5:
version "0.0.5"
resolved "https://registry.npmjs.org/seq-queue/-/seq-queue-0.0.5.tgz"
@ -6856,6 +7184,16 @@ serve-static@1.15.0:
parseurl "~1.3.3"
send "0.18.0"
serve-static@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-2.1.0.tgz#1b4eacbe93006b79054faa4d6d0a501d7f0e84e2"
integrity sha512-A3We5UfEjG8Z7VkDv6uItWw6HY2bBSBJT1KtVESn6EOoOr2jAxNhxWCLY3jDE2WcuHXByWju74ck3ZgLwL8xmA==
dependencies:
encodeurl "^2.0.0"
escape-html "^1.0.3"
parseurl "^1.3.3"
send "^1.0.0"
set-blocking@^2.0.0:
version "2.0.0"
resolved "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz"
@ -6914,6 +7252,35 @@ shebang-regex@^3.0.0:
resolved "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz"
integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==
side-channel-list@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/side-channel-list/-/side-channel-list-1.0.0.tgz#10cb5984263115d3b7a0e336591e290a830af8ad"
integrity sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==
dependencies:
es-errors "^1.3.0"
object-inspect "^1.13.3"
side-channel-map@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/side-channel-map/-/side-channel-map-1.0.1.tgz#d6bb6b37902c6fef5174e5f533fab4c732a26f42"
integrity sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==
dependencies:
call-bound "^1.0.2"
es-errors "^1.3.0"
get-intrinsic "^1.2.5"
object-inspect "^1.13.3"
side-channel-weakmap@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz#11dda19d5368e40ce9ec2bdc1fb0ecbc0790ecea"
integrity sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==
dependencies:
call-bound "^1.0.2"
es-errors "^1.3.0"
get-intrinsic "^1.2.5"
object-inspect "^1.13.3"
side-channel-map "^1.0.1"
side-channel@^1.0.4, side-channel@^1.0.6:
version "1.0.6"
resolved "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz"
@ -6924,6 +7291,17 @@ side-channel@^1.0.4, side-channel@^1.0.6:
get-intrinsic "^1.2.4"
object-inspect "^1.13.1"
side-channel@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.1.0.tgz#c3fcff9c4da932784873335ec9765fa94ff66bc9"
integrity sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==
dependencies:
es-errors "^1.3.0"
object-inspect "^1.13.3"
side-channel-list "^1.0.0"
side-channel-map "^1.0.1"
side-channel-weakmap "^1.0.2"
signal-exit@^3.0.0, signal-exit@^3.0.3:
version "3.0.7"
resolved "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz"
@ -6987,7 +7365,7 @@ stack-trace@0.0.x:
resolved "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz"
integrity sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==
statuses@2.0.1:
statuses@2.0.1, statuses@^2.0.1:
version "2.0.1"
resolved "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz"
integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==
@ -7361,6 +7739,15 @@ type-is@^1.6.4, type-is@~1.6.18:
media-typer "0.3.0"
mime-types "~2.1.24"
type-is@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/type-is/-/type-is-2.0.0.tgz#7d249c2e2af716665cc149575dadb8b3858653af"
integrity sha512-gd0sGezQYCbWSbkZr75mln4YBidWUN60+devscpLF5mtRDUpiaTvKpBNrdaCvel1NdR2k6vclXybU5fBd2i+nw==
dependencies:
content-type "^1.0.5"
media-typer "^1.1.0"
mime-types "^3.0.0"
typed-array-buffer@^1.0.2:
version "1.0.2"
resolved "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz"
@ -7780,7 +8167,17 @@ zod-to-json-schema@^3.22.3, zod-to-json-schema@^3.22.4, zod-to-json-schema@^3.22
resolved "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.23.0.tgz"
integrity sha512-az0uJ243PxsRIa2x1WmNE/pnuA05gUq/JB8Lwe1EDCCL/Fz9MgjYQ0fPlyc2Tcv6aF2ZA7WM5TWaRZVEFaAIag==
zod-to-json-schema@^3.24.1:
version "3.24.5"
resolved "https://registry.yarnpkg.com/zod-to-json-schema/-/zod-to-json-schema-3.24.5.tgz#d1095440b147fb7c2093812a53c54df8d5df50a3"
integrity sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g==
zod@^3.22.3, zod@^3.22.4:
version "3.23.5"
resolved "https://registry.npmjs.org/zod/-/zod-3.23.5.tgz"
integrity sha512-fkwiq0VIQTksNNA131rDOsVJcns0pfVUjHzLrNBiF/O/Xxb5lQyEXkhZWcJ7npWsYlvs+h0jFWXXy4X46Em1JA==
zod@^3.23.8:
version "3.24.2"
resolved "https://registry.yarnpkg.com/zod/-/zod-3.24.2.tgz#8efa74126287c675e92f46871cfc8d15c34372b3"
integrity sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==