server: enable mcp proxy (#1904)

* update http lib

* Add cors proxy

---------

Co-authored-by: firecoperana <firecoperana>
This commit is contained in:
firecoperana 2026-06-04 08:43:07 -05:00 committed by GitHub
parent 074fc7dafd
commit 6c0180d702
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 11466 additions and 2713 deletions

View File

@ -71,6 +71,7 @@ add_library(${TARGET} STATIC
train.cpp
log.cpp
log.h
http.h
ngram-cache.cpp
ngram-cache.h
ngram-map.cpp

View File

@ -2397,6 +2397,10 @@ bool gpt_params_find_arg(int argc, char ** argv, const std::string & arg, gpt_pa
params.webui = common_webui_from_name(std::string(argv[i]));
return true;
}
if (arg == "--webui-mcp-proxy" || arg == "--ui-mcp-proxy") {
params.webui_mcp_proxy = true;
return true;
}
if (arg == "--api-key") {
CHECK_ARG
params.api_keys.push_back(argv[i]);
@ -3234,6 +3238,7 @@ void gpt_params_print_usage(int /*argc*/, char ** argv, const gpt_params & param
"- auto: default webui \n"
"- llamacpp: llamacpp webui \n"
"(default: auto)", });
options.push_back({ "server", " --ui-mcp-proxy, --webui-mcp-proxy", "experimental: whether to enable MCP CORS proxy - do not enable in untrusted environments (default: disabled)" });
options.push_back({ "server", " --api-key KEY", "API key to use for authentication (default: none)" });
options.push_back({ "server", " --api-key-file FNAME", "path to file containing API keys (default: none)" });
options.push_back({ "server", " --ssl-key-file FNAME", "path to file a PEM-encoded SSL private key" });

View File

@ -501,6 +501,7 @@ struct gpt_params {
// "advanced" endpoints are disabled by default for better security
common_webui webui = COMMON_WEBUI_AUTO;
bool webui_mcp_proxy = false;
bool endpoint_slots = true;
bool endpoint_props = false; // only control POST requests, not GET
bool endpoint_metrics = false;

99
common/http.h Normal file
View File

@ -0,0 +1,99 @@
#pragma once
#include <cpp-httplib/httplib.h>
struct common_http_url {
std::string scheme;
std::string user;
std::string password;
std::string host;
int port;
std::string path;
};
static common_http_url common_http_parse_url(const std::string & url) {
common_http_url parts;
auto scheme_end = url.find("://");
if (scheme_end == std::string::npos) {
throw std::runtime_error("invalid URL: no scheme");
}
parts.scheme = url.substr(0, scheme_end);
if (parts.scheme != "http" && parts.scheme != "https") {
throw std::runtime_error("unsupported URL scheme: " + parts.scheme);
}
auto rest = url.substr(scheme_end + 3);
auto at_pos = rest.find('@');
if (at_pos != std::string::npos) {
auto auth = rest.substr(0, at_pos);
auto colon_pos = auth.find(':');
if (colon_pos != std::string::npos) {
parts.user = auth.substr(0, colon_pos);
parts.password = auth.substr(colon_pos + 1);
} else {
parts.user = auth;
}
rest = rest.substr(at_pos + 1);
}
auto slash_pos = rest.find('/');
if (slash_pos != std::string::npos) {
parts.host = rest.substr(0, slash_pos);
parts.path = rest.substr(slash_pos);
} else {
parts.host = rest;
parts.path = "/";
}
auto colon_pos = parts.host.find(':');
if (colon_pos != std::string::npos) {
parts.port = std::stoi(parts.host.substr(colon_pos + 1));
parts.host = parts.host.substr(0, colon_pos);
} else if (parts.scheme == "http") {
parts.port = 80;
} else if (parts.scheme == "https") {
parts.port = 443;
} else {
throw std::runtime_error("unsupported URL scheme: " + parts.scheme);
}
return parts;
}
static std::pair<httplib::Client, common_http_url> common_http_client(const std::string & url) {
common_http_url parts = common_http_parse_url(url);
if (parts.host.empty()) {
throw std::runtime_error("error: invalid URL format");
}
#ifndef CPPHTTPLIB_OPENSSL_SUPPORT
if (parts.scheme == "https") {
throw std::runtime_error(
"HTTPS is not supported. Please rebuild with one of:\n"
" -DLLAMA_BUILD_BORINGSSL=ON\n"
" -DLLAMA_BUILD_LIBRESSL=ON\n"
" -DLLAMA_OPENSSL=ON (default, requires OpenSSL dev files installed)"
);
}
#endif
httplib::Client cli(parts.scheme + "://" + parts.host + ":" + std::to_string(parts.port));
if (!parts.user.empty()) {
cli.set_basic_auth(parts.user, parts.password);
}
cli.set_follow_location(true);
return { std::move(cli), std::move(parts) };
}
static std::string common_http_show_masked_url(const common_http_url & parts) {
return parts.scheme + "://" + (parts.user.empty() ? "" : "****:****@") + parts.host + parts.path;
}

View File

@ -0,0 +1,170 @@
#pragma once
#include "common.h"
#include "http.h"
#include <string>
#include <unordered_set>
#include <list>
#include <map>
static std::string to_lower_copy(const std::string & value) {
std::string lowered(value.size(), '\0');
std::transform(value.begin(), value.end(), lowered.begin(), [](unsigned char c) { return std::tolower(c); });
return lowered;
}
static httplib::Request prepare_proxy_req_header(const std::string & method,
const std::string & scheme,
const std::string & host,
int port,
const std::string & path,
const std::map<std::string, std::string> & headers,
const std::string & body,
const httplib::FormFiles & files) {
httplib::Request req;
bool has_files = !files.empty();
req.form.files = files;
std::string effective_body = body;
std::string override_content_type;
req.method = method;
req.path = path;
for (const auto & [key, value] : headers) {
const auto lowered = to_lower_copy(key);
if (lowered == "accept-encoding") {
// disable Accept-Encoding to avoid compressed responses
continue;
}
if (lowered == "transfer-encoding") {
// the body is already decoded
continue;
}
if (lowered == "content-length") {
// let httplib calculate Content-Length from the actual body
continue;
}
if (lowered == "content-type") {
if (has_files) {
// we set our own Content-Type with the new boundary
continue;
}
// when no files but the original request was multipart,
// the body is now JSON, so correct the Content-Type
if (value.find("multipart/form-data") != std::string::npos) {
override_content_type = "application/json; charset=utf-8";
continue;
}
}
if (lowered == "host") {
bool is_default_port = (scheme == "https" && port == 443) || (scheme == "http" && port == 80);
req.set_header(key, is_default_port ? host : host + ":" + std::to_string(port));
} else {
req.set_header(key, value);
}
}
req.body = effective_body;
if (!override_content_type.empty()) {
req.set_header("Content-Type", override_content_type);
}
//req.response_handler = response_handler;
//req.content_receiver = content_receiver;
return req;
}
static std::string get_param(httplib::Params params,const std::string & key, const std::string & def = "") {
auto it = params.find("url");
if (it != params.end()) {
return it->second;
}
return def;
}
static void proxy_request(const httplib::Request & req,
httplib::Response & res,
const std::string & method) {
std::string target_url = get_param(req.params, "url");
common_http_url parsed_url = common_http_parse_url(target_url);
if (parsed_url.host.empty()) {
throw std::runtime_error("invalid target URL: missing host");
}
if (parsed_url.path.empty()) {
parsed_url.path = "/";
}
if (!parsed_url.password.empty()) {
throw std::runtime_error("authentication in target URL is not supported");
}
if (parsed_url.scheme != "http" && parsed_url.scheme != "https") {
throw std::runtime_error("unsupported URL scheme in target URL: " + parsed_url.scheme);
}
SRV_INF("proxying %s request to %s://%s:%i%s\n", method.c_str(), parsed_url.scheme.c_str(), parsed_url.host.c_str(), parsed_url.port, parsed_url.path.c_str());
std::map<std::string, std::string> headers;
for (auto [key, value] : req.headers) {
auto new_key = key;
if (string_starts_with(new_key, "x-proxy-header-")) {
string_replace_all(new_key, "x-proxy-header-", "");
}
headers[new_key] = value;
}
httplib::Request proxy_req = prepare_proxy_req_header(method,
parsed_url.scheme,
parsed_url.host,
parsed_url.port,
parsed_url.path,
headers,
req.body,
req.form.files);
// Make the proxied request
httplib::Result proxy_res;
if (parsed_url.scheme == "https") {
#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
httplib::SSLClient cli(parsed_url.host, parsed_url.port);
// set timeouts, follow redirects as needed
cli.set_connection_timeout(600);
cli.set_read_timeout(600);
cli.set_write_timeout(600);
cli.set_follow_location(true);
proxy_res = cli.send(proxy_req);
#else
res.status = 501;
res.set_content("HTTPS not supported (build with OpenSSL)", "text/plain");
return;
#endif
} else {
httplib::Client cli(parsed_url.host, parsed_url.port);
cli.set_connection_timeout(600);
cli.set_read_timeout(600);
cli.set_write_timeout(600);
proxy_res = cli.send(std::move(proxy_req));
}
if (!proxy_res) {
std::string error_data = "Proxy failed: " + httplib::to_string(proxy_res.error());
json final_response{ {"error", error_data} };
res.set_content(safe_json_to_str(final_response), "application/json; charset=utf-8");
res.status = json_value(error_data, "code", 500);
return;
}
res.status = proxy_res->status;
res.set_content(proxy_res->body, proxy_res->get_header_value("Content-Type"));
for (const auto & h : proxy_res->headers) {
// skip hop-by-hop headers
if (h.first != "Transfer-Encoding" && h.first != "Connection")
res.set_header(h.first, h.second);
}
}
static void proxy_handler_get(const httplib::Request & req, httplib::Response & res) {
proxy_request(req, res, "GET");
}
static void proxy_handler_post(const httplib::Request & req, httplib::Response & res) {
proxy_request(req, res, "POST");
}

View File

@ -2,6 +2,7 @@
#include "server-context.h"
#include "server-common.h"
#include "server-chat.h"
#include "server-cors-proxy.h"
#include "chat.h"
#include "common.h"
@ -1020,7 +1021,8 @@ int main(int argc, char ** argv) {
{"vision", ctx_server.chat_params.allow_image},
{"audio", ctx_server.chat_params.allow_audio},
} },
{ "n_ctx", ctx_server.n_ctx }
{ "n_ctx", ctx_server.n_ctx },
{ "cors_proxy_enabled", ctx_server.params_base.webui_mcp_proxy},
};
@ -2108,6 +2110,16 @@ int main(int argc, char ** argv) {
}
#endif
}
// CORS proxy (EXPERIMENTAL, only used by the Web UI for MCP)
if (params.webui_mcp_proxy) {
SRV_WRN("%s", "-----------------\n");
SRV_WRN("%s", "CORS proxy is enabled, do not expose server to untrusted environments\n");
SRV_WRN("%s", "This feature is EXPERIMENTAL and may be removed or changed in future versions\n");
SRV_WRN("%s", "-----------------\n");
svr->Get("/cors-proxy", proxy_handler_get);
svr->Post("/cors-proxy", proxy_handler_post);
}
//
// Start the server
//

View File

@ -22,7 +22,93 @@ target_compile_definitions(${TARGET} PRIVATE
CPPHTTPLIB_TCP_NODELAY=1
)
if (LLAMA_OPENSSL)
set(OPENSSL_NO_ASM ON CACHE BOOL "Disable OpenSSL ASM code when building BoringSSL or LibreSSL")
if (LLAMA_BUILD_BORINGSSL)
set(FIPS OFF CACHE BOOL "Enable FIPS (BoringSSL)")
set(BORINGSSL_GIT "https://boringssl.googlesource.com/boringssl" CACHE STRING "BoringSSL git repository")
set(BORINGSSL_VERSION "0.20260508.0" CACHE STRING "BoringSSL version")
message(STATUS "Fetching BoringSSL version ${BORINGSSL_VERSION}")
set(BORINGSSL_ARGS
GIT_REPOSITORY ${BORINGSSL_GIT}
GIT_TAG ${BORINGSSL_VERSION}
)
if(CMAKE_VERSION VERSION_GREATER_EQUAL 3.28)
list(APPEND BORINGSSL_ARGS EXCLUDE_FROM_ALL)
endif()
include(FetchContent)
FetchContent_Declare(boringssl ${BORINGSSL_ARGS})
set(SAVED_BUILD_SHARED_LIBS ${BUILD_SHARED_LIBS})
set(SAVED_BUILD_TESTING ${BUILD_TESTING})
set(BUILD_SHARED_LIBS OFF)
set(BUILD_TESTING OFF)
if(CMAKE_VERSION VERSION_GREATER_EQUAL 3.28)
FetchContent_MakeAvailable(boringssl)
else()
FetchContent_GetProperties(boringssl)
if(NOT boringssl_POPULATED)
FetchContent_Populate(boringssl)
add_subdirectory(${boringssl_SOURCE_DIR} ${boringssl_BINARY_DIR} EXCLUDE_FROM_ALL)
endif()
endif()
set(BUILD_SHARED_LIBS ${SAVED_BUILD_SHARED_LIBS})
set(BUILD_TESTING ${SAVED_BUILD_TESTING})
set(CPPHTTPLIB_OPENSSL_SUPPORT TRUE)
target_link_libraries(${TARGET} PUBLIC ssl crypto)
elseif (LLAMA_BUILD_LIBRESSL)
set(LIBRESSL_VERSION "4.3.1" CACHE STRING "LibreSSL version")
message(STATUS "Fetching LibreSSL version ${LIBRESSL_VERSION}")
set(LIBRESSL_ARGS
URL "https://cdn.openbsd.org/pub/OpenBSD/LibreSSL/libressl-${LIBRESSL_VERSION}.tar.gz"
)
if(CMAKE_VERSION VERSION_GREATER_EQUAL 3.24)
list(APPEND LIBRESSL_ARGS DOWNLOAD_EXTRACT_TIMESTAMP TRUE)
endif()
if(CMAKE_VERSION VERSION_GREATER_EQUAL 3.28)
list(APPEND LIBRESSL_ARGS EXCLUDE_FROM_ALL)
endif()
include(FetchContent)
FetchContent_Declare(libressl ${LIBRESSL_ARGS})
set(SAVED_BUILD_SHARED_LIBS ${BUILD_SHARED_LIBS})
set(SAVED_BUILD_TESTING ${BUILD_TESTING})
set(BUILD_SHARED_LIBS OFF)
set(BUILD_TESTING OFF)
if(CMAKE_VERSION VERSION_GREATER_EQUAL 3.28)
FetchContent_MakeAvailable(libressl)
else()
FetchContent_GetProperties(libressl)
if(NOT libressl_POPULATED)
FetchContent_Populate(libressl)
add_subdirectory(${libressl_SOURCE_DIR} ${libressl_BINARY_DIR} EXCLUDE_FROM_ALL)
endif()
endif()
set(BUILD_SHARED_LIBS ${SAVED_BUILD_SHARED_LIBS})
set(BUILD_TESTING ${SAVED_BUILD_TESTING})
set(CPPHTTPLIB_OPENSSL_SUPPORT TRUE)
target_link_libraries(${TARGET} PUBLIC ssl crypto)
elseif (LLAMA_OPENSSL)
find_package(OpenSSL)
if (OpenSSL_FOUND)
include(CheckCSourceCompiles)
@ -44,17 +130,51 @@ if (LLAMA_OPENSSL)
set(CMAKE_REQUIRED_INCLUDES ${SAVED_CMAKE_REQUIRED_INCLUDES})
if (OPENSSL_VERSION_SUPPORTED)
message(STATUS "OpenSSL found: ${OPENSSL_VERSION}")
target_compile_definitions(${TARGET} PUBLIC CPPHTTPLIB_OPENSSL_SUPPORT)
set(CPPHTTPLIB_OPENSSL_SUPPORT TRUE)
target_link_libraries(${TARGET} PUBLIC OpenSSL::SSL OpenSSL::Crypto)
if (APPLE AND CMAKE_SYSTEM_NAME STREQUAL "Darwin")
target_compile_definitions(${TARGET} PUBLIC CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN)
find_library(CORE_FOUNDATION_FRAMEWORK CoreFoundation REQUIRED)
find_library(SECURITY_FRAMEWORK Security REQUIRED)
target_link_libraries(${TARGET} PUBLIC ${CORE_FOUNDATION_FRAMEWORK} ${SECURITY_FRAMEWORK})
endif()
endif()
else()
message(STATUS "OpenSSL not found, SSL support disabled")
message(WARNING "OpenSSL not found, HTTPS support disabled")
endif()
endif()
# disable warnings in 3rd party code
if(LLAMA_BUILD_BORINGSSL OR LLAMA_BUILD_LIBRESSL)
if (CMAKE_CXX_COMPILER_ID STREQUAL "MSVC")
target_compile_options(ssl PRIVATE /w)
target_compile_options(crypto PRIVATE /w)
if(LLAMA_BUILD_BORINGSSL)
target_compile_options(fipsmodule PRIVATE /w)
endif()
if(LLAMA_BUILD_LIBRESSL)
target_compile_options(ssl_obj PRIVATE /w)
target_compile_options(bs_obj PRIVATE /w)
target_compile_options(compat_obj PRIVATE /w)
target_compile_options(crypto_obj PRIVATE /w)
endif()
else()
target_compile_options(ssl PRIVATE -w)
target_compile_options(crypto PRIVATE -w)
if(LLAMA_BUILD_BORINGSSL)
target_compile_options(fipsmodule PRIVATE -w)
endif()
if(LLAMA_BUILD_LIBRESSL)
target_compile_options(ssl_obj PRIVATE -w)
target_compile_options(bs_obj PRIVATE -w)
target_compile_options(compat_obj PRIVATE -w)
target_compile_options(crypto_obj PRIVATE -w)
endif()
endif()
endif()
if (CPPHTTPLIB_OPENSSL_SUPPORT)
target_compile_definitions(${TARGET} PUBLIC CPPHTTPLIB_OPENSSL_SUPPORT) # used in server.cpp
if (APPLE AND CMAKE_SYSTEM_NAME STREQUAL "Darwin")
find_library(CORE_FOUNDATION_FRAMEWORK CoreFoundation REQUIRED)
find_library(SECURITY_FRAMEWORK Security REQUIRED)
target_link_libraries(${TARGET} PUBLIC ${CORE_FOUNDATION_FRAMEWORK} ${SECURITY_FRAMEWORK})
endif()
if (WIN32 AND NOT MSVC)
target_link_libraries(${TARGET} PUBLIC crypt32)
endif()
endif()

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff