diff --git a/.env.example b/.env.example
index 9981c6c..be75612 100644
--- a/.env.example
+++ b/.env.example
@@ -181,6 +181,12 @@ BYPASS_MODEL_ACCESS_CONTROL=true
WEBUI_NAME=UltiAI
AI_ASSISTANT_PUBLIC_PATH=/ai
ULTIMAIL_MCP_URL=http://ultimail-mcp:3100
+# Public MCP endpoint (nginx → ultid → ultimail-mcp): /api/v1/ai/mcp
+# OpenWebUI uses AI_GATEWAY_API_KEY + forwarded X-OpenWebUI-User-* headers for per-user tokens.
+# Ultimail MCP auto-enabled for all users (see deploy/openwebui/docker-compose.openwebui.yml):
+# DEFAULT_MODEL_PARAMS={"function_calling":"native"}
+# DEFAULT_MODEL_METADATA={"toolIds":["server:mcp:ultimail"],...}
+# TOOL_SERVER_CONNECTIONS with config.access_grants public read
# OpenWebUI utilise POSTGRES_USER/POSTGRES_PASSWORD (base openwebui créée dans init-db.sh)
# -----------------------------------------------------------------------------
diff --git a/deploy/docker-compose.yml b/deploy/docker-compose.yml
index 37faef0..ce601eb 100644
--- a/deploy/docker-compose.yml
+++ b/deploy/docker-compose.yml
@@ -6,6 +6,8 @@ services:
- "80:80"
volumes:
- ./nginx/default.conf.template:/etc/nginx/templates/default.conf.template:ro
+ - ./nginx/openwebui-subfilters.conf:/etc/nginx/openwebui-subfilters.conf:ro
+ - ./nginx/patches/QGuclOcQ.js:/etc/nginx/patches/QGuclOcQ.js:ro
environment:
DOMAIN: ${DOMAIN:-localhost}
MAIL_FRONTEND_UPSTREAM: ${MAIL_FRONTEND_UPSTREAM:-host.docker.internal:3004}
diff --git a/deploy/nginx/default.conf.template b/deploy/nginx/default.conf.template
index 908c9e0..d1b2fb9 100644
--- a/deploy/nginx/default.conf.template
+++ b/deploy/nginx/default.conf.template
@@ -180,6 +180,40 @@ server {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
+ location ^~ /api/v1/ai/mcp {
+ resolver 127.0.0.11 valid=10s ipv6=off;
+ set $ultid_upstream ultid:8080;
+ proxy_hide_header Access-Control-Allow-Origin;
+ proxy_hide_header Access-Control-Allow-Methods;
+ proxy_hide_header Access-Control-Allow-Headers;
+ proxy_hide_header Access-Control-Expose-Headers;
+ proxy_hide_header Access-Control-Max-Age;
+ proxy_hide_header Access-Control-Allow-Credentials;
+ proxy_hide_header Vary;
+ add_header Access-Control-Allow-Origin $cors_allow_origin always;
+ add_header Access-Control-Allow-Methods "GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS" always;
+ add_header Access-Control-Allow-Headers "Accept, Authorization, Content-Type, Idempotency-Key, Mcp-Session-Id, Origin, X-Requested-With, X-Trace-Id, X-Ulti-Token, X-OpenWebUI-User-Email, X-OpenWebUI-User-Id, X-OpenWebUI-User-Name, X-OpenWebUI-User-Role" always;
+ add_header Access-Control-Expose-Headers "Mcp-Session-Id, X-Trace-Id" always;
+ add_header Access-Control-Max-Age 300 always;
+ add_header Vary Origin always;
+ proxy_pass http://$ultid_upstream;
+ proxy_http_version 1.1;
+ proxy_set_header Host $host;
+ proxy_set_header Cookie $http_cookie;
+ proxy_set_header Authorization $http_authorization;
+ proxy_set_header X-Ulti-Token $http_x_ulti_token;
+ proxy_set_header X-OpenWebUI-User-Email $http_x_openwebui_user_email;
+ proxy_set_header X-OpenWebUI-User-Id $http_x_openwebui_user_id;
+ proxy_set_header X-OpenWebUI-User-Name $http_x_openwebui_user_name;
+ proxy_set_header X-OpenWebUI-User-Role $http_x_openwebui_user_role;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ proxy_buffering off;
+ proxy_cache off;
+ proxy_read_timeout 86400s;
+ proxy_send_timeout 86400s;
+ }
location ^~ /api/v1/ai/ {
resolver 127.0.0.11 valid=10s ipv6=off;
set $ultid_upstream ultid:8080;
@@ -192,16 +226,27 @@ server {
proxy_hide_header Vary;
add_header Access-Control-Allow-Origin $cors_allow_origin always;
add_header Access-Control-Allow-Methods "GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS" always;
- add_header Access-Control-Allow-Headers "Accept, Authorization, Content-Type, Idempotency-Key, Origin, X-Requested-With, X-Trace-Id" always;
- add_header Access-Control-Expose-Headers "X-Trace-Id" always;
+ add_header Access-Control-Allow-Headers "Accept, Authorization, Content-Type, Idempotency-Key, Mcp-Session-Id, Origin, X-Requested-With, X-Trace-Id, X-Ulti-Token, X-OpenWebUI-User-Email, X-OpenWebUI-User-Id, X-OpenWebUI-User-Name, X-OpenWebUI-User-Role" always;
+ add_header Access-Control-Expose-Headers "Mcp-Session-Id, X-Trace-Id" always;
add_header Access-Control-Max-Age 300 always;
add_header Vary Origin always;
proxy_pass http://$ultid_upstream;
proxy_http_version 1.1;
proxy_set_header Host $host;
+ proxy_set_header Cookie $http_cookie;
+ proxy_set_header Authorization $http_authorization;
+ proxy_set_header X-Ulti-Token $http_x_ulti_token;
+ proxy_set_header X-OpenWebUI-User-Email $http_x_openwebui_user_email;
+ proxy_set_header X-OpenWebUI-User-Id $http_x_openwebui_user_id;
+ proxy_set_header X-OpenWebUI-User-Name $http_x_openwebui_user_name;
+ proxy_set_header X-OpenWebUI-User-Role $http_x_openwebui_user_role;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
+ proxy_buffering off;
+ proxy_cache off;
+ proxy_read_timeout 86400s;
+ proxy_send_timeout 86400s;
}
location ^~ /api/v1/users/me {
resolver 127.0.0.11 valid=10s ipv6=off;
@@ -446,6 +491,27 @@ server {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
+ proxy_buffering off;
+ proxy_cache off;
+ proxy_read_timeout 86400s;
+ proxy_send_timeout 86400s;
+ }
+
+ # OpenWebUI socket.io under /ai prefix (SvelteKit base /ai — avoid embed-auth on /ai/)
+ location ^~ /ai/ws/socket.io {
+ resolver 127.0.0.11 valid=10s ipv6=off;
+ set $openwebui_upstream openwebui:8080;
+ rewrite ^/ai/ws/socket.io(.*)$ /ws/socket.io$1 break;
+ proxy_pass http://$openwebui_upstream;
+ proxy_http_version 1.1;
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection $connection_upgrade;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ proxy_buffering off;
+ proxy_cache off;
proxy_read_timeout 86400s;
proxy_send_timeout 86400s;
}
@@ -648,6 +714,13 @@ server {
# SvelteKit base path — without this, /ai/ routes 404 (base "" expects site root)
sub_filter 'base: ""' 'base: "/ai"';
sub_filter "base: ''" 'base: "/ai"';
+ # Keep t="" so socket.io uses default namespace (/), not /ai — see openwebui-subfilters.conf
+ include /etc/nginx/openwebui-subfilters.conf;
+ sub_filter 'location.href="/auth"' 'location.href="/ai/auth"';
+ sub_filter "location.href='/auth'" "location.href='/ai/auth'";
+ sub_filter '"/auth?"' '"/ai/auth?"';
+ sub_filter '"/auth"' '"/ai/auth"';
+ sub_filter '