feat(nextcloud): serve MCP through the Context Agent ExApp

Nextcloud carries no MCP server of its own, so the role runs the Context
Agent as an ExApp: render its container while MCP is enabled, wait for
its heartbeat, register it with AppAPI, add the MCP route, enable it, and
only then mint the app password clients present.

Split the credentials by origin. The shared secret between the AppAPI
proxy and the ExApp is generated by this deployment and stays a vault
credential; the app password is minted by Nextcloud through occ and is
persisted through sys-token-store instead.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
kevinveenbirkenbachandClaude Opus 5 committed 2026-08-02 22:24:33 +02:00
1 parent f2fe5e94b1
commit a2be4f081a
16 files changed
+495 -7

No files matched your search

+59
View File
@@ -24,12 +24,14 @@ flowchart LR
dep_web_app_dashboard["web-app-dashboard 🐳🐝"]
dep_web_app_discourse["web-app-discourse 🐳🐝"]
dep_web_app_gitlab["web-app-gitlab 🐳🐝"]
dep_web_app_hermes["web-app-hermes 🐳🐝"]
dep_web_app_keycloak["web-app-keycloak 🐳🐝"]
dep_web_app_mailu["web-app-mailu 🐳🐝"]
dep_web_app_mastodon["web-app-mastodon 🐳🐝"]
dep_web_app_matomo["web-app-matomo 🐳🐝"]
dep_web_app_matrix["web-app-matrix 🐳🐝"]
dep_web_app_mattermost["web-app-mattermost 🐳🐝"]
dep_web_app_openclaw["web-app-openclaw 🐳🐝"]
dep_web_app_openproject["web-app-openproject 🐳🐝"]
dep_web_app_openwebui["web-app-openwebui 🐳🐝"]
dep_web_app_peertube["web-app-peertube 🐳🐝"]
@@ -55,6 +57,8 @@ flowchart LR
svc_mariadb["mariadb"]
svc_nextcloud["nextcloud"]
svc_proxy["proxy"]
svc_mcp["mcp"]
svc_context_agent["context_agent"]
svc_cron["cron"]
svc_talk["talk"]
svc_whiteboard["whiteboard"]
@@ -91,12 +95,16 @@ flowchart LR
dep_web_app_dashboard -. "0..1" .-> svc_dashboard
dep_web_app_discourse -. "0..1" .-> svc_discourse
dep_web_app_gitlab -. "0..1" .-> svc_gitlab
dep_web_app_hermes -. "0..1" .-> svc_context_agent
dep_web_app_hermes -. "0..1" .-> svc_mcp
dep_web_app_keycloak -. "0..1" .-> svc_sso
dep_web_app_mailu -. "0..1" .-> svc_email
dep_web_app_mastodon -. "0..1" .-> svc_mastodon
dep_web_app_matomo -. "0..1" .-> svc_matomo
dep_web_app_matrix -. "0..1" .-> svc_matrix
dep_web_app_mattermost -. "0..1" .-> svc_mattermost
dep_web_app_openclaw -. "0..1" .-> svc_context_agent
dep_web_app_openclaw -. "0..1" .-> svc_mcp
dep_web_app_openproject -. "0..1" .-> svc_openproject
dep_web_app_openwebui -. "0..1" .-> svc_openwebui
dep_web_app_peertube -. "0..1" .-> svc_peertube
@@ -180,6 +188,57 @@ The enable-only appstore apps stay under `nextcloud.plugins` in [`meta/services.
The SSO (`sociallogin`) and LDAP (`user_ldap`) login surfaces are covered by the OIDC/LDAP Playwright specs (requirements 017/018).
## MCP Server
Nextcloud serves a Model Context Protocol endpoint through the AppAPI proxy of the `context_agent` ExApp.
| Property | Value |
|----------|-------|
| Endpoint | `/index.php/apps/app_api/proxy/context_agent/mcp/` on the canonical Nextcloud vhost |
| Container-network URL | `http://proxy:80/index.php/apps/app_api/proxy/context_agent/mcp/` |
| Transport | Streamable HTTP |
| Exposure | public (same nginx vhost that serves the web UI) |
| Auth | Nextcloud app password, sent as `Authorization: Bearer <app-password>` |
| Identity | the Nextcloud user the app password belongs to; every tool call runs with that user's permissions |
| Implementation | plugin (the `context_agent` ExApp container, registered through the `manual_install` deploy daemon) |
| Default state | off; `services.mcp.enabled` turns on when `web-app-hermes` or `web-app-openclaw` is deployed |
### Deployment
The `context_agent` service in [`meta/services.yml`](./meta/services.yml) renders only while `services.mcp.enabled` is true. It runs `ghcr.io/nextcloud/context_agent`, listens on its internal port for AppAPI only, and shares `credentials.context_agent_app_secret` with the ExApp registration as `APP_SECRET`.
[`tasks/09_mcp.yml`](./tasks/09_mcp.yml) waits for the ExApp heartbeat, registers the deploy daemon and the ExApp (route `^/mcp`, verbs `POST,GET,DELETE`, access level `1`), enables the ExApp, mints an app password via `occ user:auth-tokens:add`, persists it through `sys-token-store` under `users.administrator.tokens['web-app-nextcloud']`, and asserts that an authenticated `initialize` call answers `200`.
### Tool categories
The ExApp registers every tool category the Context Agent ships, read-only and mutating alike, and each call runs with the permissions of the app-password owner. List the categories and narrow the exposed set with:
```bash
occ app_api:app:config:get context_agent tool_status
occ app_api:app:config:set context_agent tool_status --value '<json map of category to bool>'
```
### Verification
```bash
curl -i -X POST \
-H 'Authorization: Bearer <app-password>' \
-H 'Accept: application/json, text/event-stream' \
-H 'Content-Type: application/json' \
--data '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"cli","version":"1"}}}' \
https://<nextcloud-domain>/index.php/apps/app_api/proxy/context_agent/mcp/
```
An unauthenticated request to the same path answers `404`; [`files/playwright/test-mcp-guest.js`](./files/playwright/test-mcp-guest.js) asserts the `>= 400`.
### Default state
Off. `services.mcp.enabled` is true only while `web-app-hermes` or `web-app-openclaw` is part of the deployment.
### How to disable
Remove the MCP client roles, or pin `services.mcp.enabled: false` for this role. The `context_agent` service is then not rendered, the ExApp is not registered with AppAPI, and no app password is minted.
## Documentation
A detailed documentation for the use and administration of Nextcloud on Infinito.Nexus you will find [here](docs/README.md).
@@ -24,6 +24,7 @@ require("./test-login-admin-native").register(shared);
require("./test-login-biber-oidc").register(shared);
require("./test-login-biber-ldap").register(shared);
require("./test-guest-persona").register(shared);
require("./test-mcp-guest").register(shared);
require("./test-biber-persona").register(shared);
require("./test-administrator-persona").register(shared);
require("./test-seaweedfs");
@@ -0,0 +1,33 @@
const { test, expect } = require("@playwright/test");
const { decodeDotenvQuotedValue } = require("./personas");
const { skipUnlessServiceEnabled } = require("./service-gating");
const mcpEndpointPath = decodeDotenvQuotedValue(process.env.NEXTCLOUD_MCP_ENDPOINT_PATH);
exports.register = function (shared) {
test("guest: the MCP endpoint rejects unauthenticated access", async ({ page }) => {
skipUnlessServiceEnabled("mcp");
expect(mcpEndpointPath, "NEXTCLOUD_MCP_ENDPOINT_PATH must be set in the Playwright env file").toBeTruthy();
const endpointUrl = new URL(mcpEndpointPath, shared.env.nextcloudBaseUrl).toString();
const response = await page.request.post(endpointUrl, {
failOnStatusCode: false,
headers: {
Accept: "application/json, text/event-stream",
"Content-Type": "application/json",
},
data: {
jsonrpc: "2.0",
id: 1,
method: "initialize",
params: { protocolVersion: "2025-06-18", capabilities: {}, clientInfo: { name: "playwright", version: "1" } },
},
});
expect(
response.status(),
"an unauthenticated MCP probe must not be served a 2xx; the endpoint is app-password guarded",
).toBeGreaterThanOrEqual(400);
});
};
+7 -2
View File
@@ -2,6 +2,11 @@
local:
subnet: 192.168.41.0/24
overlay:
modes: [swarm]
modes: [compose, swarm]
topology: shared_net
aliases: ["proxy"]
consumer:
kind: services_flags
key: mcp
proxy_resolvable: true
aliases: ["{{ lookup('domain', 'web-app-nextcloud') }}"]
proxy_aliases: ["{{ lookup('domain', 'web-app-nextcloud') }}"]
+5
View File
@@ -29,3 +29,8 @@ credentials:
description: Internal secret for AIO Talk
algorithm: base64_prefixed_32
validation: ^base64:[A-Za-z0-9+/]{43}=$
context_agent_app_secret:
description: Shared secret between the Nextcloud AppAPI proxy and the Context Agent ExApp serving the MCP endpoint
algorithm: alphanumeric
validation:
min_length: 64
+43
View File
@@ -108,6 +108,49 @@ proxy:
mem_limit: 256m
pids_limit: 512
min_storage: 226MB
ports:
internal:
http: 80
mcp:
bond: 1
enabled: "{{ 'web-app-hermes' in group_names or 'web-app-openclaw' in group_names }}"
shared: "{{ 'web-app-hermes' in group_names or 'web-app-openclaw' in group_names }}"
direction: server
source_url: https://docs.nextcloud.com/server/latest/admin_manual/ai/app_context_agent.html
minimum_version: "30"
notes: Context Agent ExApp behind the AppAPI proxy, app-password auth.
transport: streamable_http
exposure: public
auth: app_password
auth_subject: user
implementation: plugin
endpoint:
service_key: proxy
path: /index.php/apps/app_api/proxy/context_agent/mcp/
port_key: http
tools:
read_only_default: true
mutating_tools_enabled: false
# nocheck: playwright-service-flag
context_agent:
bond: 1
enabled: "{{ 'web-app-hermes' in group_names or 'web-app-openclaw' in group_names }}"
shared: "{{ 'web-app-hermes' in group_names or 'web-app-openclaw' in group_names }}"
name: nextcloud-context-agent
image: ghcr.io/nextcloud/context_agent
version: 2.7.0
backup:
no_stop_required: true
# nocheck: single-replica
replicas: 1
cpus: "1.0"
mem_reservation: 512m
mem_limit: 1g
pids_limit: 512
min_storage: 512MB
ports:
internal:
http: 23000
cron:
# nocheck: single-replica singleton cron.sh; replicas double-run background jobs on the shared data volume
replicas: 1
+48
View File
@@ -30,6 +30,12 @@
dashboard:
enabled: true
shared: true
mcp:
enabled: true
shared: true
context_agent:
enabled: true
shared: true
matomo:
enabled: true
shared: true
@@ -113,6 +119,12 @@
dashboard:
enabled: false
shared: false # nocheck: variants-static-override backup shared stays literally true
mcp:
enabled: false
shared: false
context_agent:
enabled: false
shared: false
matomo:
enabled: false
shared: false # nocheck: variants-static-override backup shared stays literally true
@@ -198,6 +210,12 @@
dashboard:
enabled: false
shared: false # nocheck: variants-static-override backup shared stays literally true
mcp:
enabled: false
shared: false
context_agent:
enabled: false
shared: false
matomo:
enabled: false
shared: false # nocheck: variants-static-override backup shared stays literally true
@@ -283,6 +301,12 @@
dashboard:
enabled: false
shared: false # nocheck: variants-static-override backup shared stays literally true
mcp:
enabled: false
shared: false
context_agent:
enabled: false
shared: false
matomo:
enabled: false
shared: false # nocheck: variants-static-override backup shared stays literally true
@@ -368,6 +392,12 @@
dashboard:
enabled: false
shared: false # nocheck: variants-static-override backup shared stays literally true
mcp:
enabled: false
shared: false
context_agent:
enabled: false
shared: false
matomo:
enabled: false
shared: false # nocheck: variants-static-override backup shared stays literally true
@@ -453,6 +483,12 @@
dashboard:
enabled: false
shared: false # nocheck: variants-static-override backup shared stays literally true
mcp:
enabled: false
shared: false
context_agent:
enabled: false
shared: false
matomo:
enabled: false
shared: false # nocheck: variants-static-override backup shared stays literally true
@@ -538,6 +574,12 @@
dashboard:
enabled: false
shared: false # nocheck: variants-static-override backup shared stays literally true
mcp:
enabled: false
shared: false
context_agent:
enabled: false
shared: false
matomo:
enabled: false
shared: false # nocheck: variants-static-override backup shared stays literally true
@@ -623,6 +665,12 @@
dashboard:
enabled: false
shared: false # nocheck: variants-static-override backup shared stays literally true
mcp:
enabled: false
shared: false
context_agent:
enabled: false
shared: false
matomo:
enabled: false
shared: false # nocheck: variants-static-override backup shared stays literally true
@@ -37,3 +37,7 @@
- name: 🔧 Reconcile administrator identities after plugin setup
include_tasks: 07_admin_identities.yml
- name: "🔌 Provision the MCP server surface"
include_tasks: 09_mcp.yml
when: NEXTCLOUD_MCP_ENABLED | bool
+73
View File
@@ -0,0 +1,73 @@
---
- name: "🔍 Resolve the Nextcloud hosting node + container id"
ansible.builtin.include_tasks: "{{ lookup('path_absolute', 'roles/sys-svc-compose/tasks/utils/swarm/resolve_host_cid.yml') }}"
vars:
resolve_service_key: "{{ NEXTCLOUD_SERVICE }}"
resolve_output_prefix: NEXTCLOUD
resolve_compose_cid: "{{ NEXTCLOUD_CONTAINER }}"
- name: "🔍 Resolve the proxy hosting node + container id"
ansible.builtin.include_tasks: "{{ lookup('path_absolute', 'roles/sys-svc-compose/tasks/utils/swarm/resolve_host_cid.yml') }}"
vars:
resolve_service_key: "{{ NEXTCLOUD_PROXY_SERVICE }}"
resolve_output_prefix: NEXTCLOUD_PROXY
resolve_compose_cid: "{{ NEXTCLOUD_PROXY_CONTAINER }}"
- name: "🔍 Resolve the Context Agent hosting node + container id"
ansible.builtin.include_tasks: "{{ lookup('path_absolute', 'roles/sys-svc-compose/tasks/utils/swarm/resolve_host_cid.yml') }}"
vars:
resolve_service_key: "{{ NEXTCLOUD_CONTEXT_AGENT_SERVICE }}"
resolve_output_prefix: NEXTCLOUD_CONTEXT_AGENT
resolve_compose_cid: "{{ NEXTCLOUD_CONTEXT_AGENT_CONTAINER }}"
- name: "⏳ Wait until the Context Agent answers its heartbeat"
ansible.builtin.command:
argv:
- container
- exec
- "{{ NEXTCLOUD_CONTEXT_AGENT_LOCAL_CID }}" # nocheck: container-exec-resolver
- python3
- -c
- "import urllib.request; urllib.request.urlopen('http://127.0.0.1:{{ NEXTCLOUD_CONTEXT_AGENT_PORT }}/heartbeat', timeout=5)"
register: nextcloud_context_agent_heartbeat
retries: 60
delay: 5
until: nextcloud_context_agent_heartbeat.rc == 0
changed_when: false
delegate_to: "{{ NEXTCLOUD_CONTEXT_AGENT_HOST_NODE }}"
run_once: true
- name: "🚀 Register the Context Agent as an AppAPI ExApp"
ansible.builtin.include_tasks:
file: 09_mcp/register.yml
apply:
delegate_to: "{{ NEXTCLOUD_HOST_NODE }}"
run_once: true
- name: "🔍 Read the MCP app password this deployment already issued"
ansible.builtin.set_fact:
nextcloud_mcp_token: >-
{{ lookup('users', 'administrator').tokens[application_id] | default('') | trim }}
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
- name: "🩺 Check the stored app password against the MCP endpoint"
ansible.builtin.include_tasks: 09_mcp/probe.yml
when: nextcloud_mcp_token | length > 0
- name: "🔑 Mint and persist a fresh MCP app password"
ansible.builtin.include_tasks: 09_mcp/token.yml
when: nextcloud_mcp_token | length == 0 or nextcloud_mcp_probe.stdout | default('') != '200'
- name: "🩺 Re-check the freshly minted app password"
ansible.builtin.include_tasks: 09_mcp/probe.yml
when: nextcloud_mcp_probe is not defined or nextcloud_mcp_probe.stdout != '200'
- name: "🛑 Refuse an MCP endpoint that rejects the stored app password"
ansible.builtin.assert:
that:
- nextcloud_mcp_probe.stdout == '200'
fail_msg: >-
The MCP endpoint {{ NEXTCLOUD_MCP_PATH }} answered
{{ nextcloud_mcp_probe.stdout | default('no status') }} for the app password stored
for {{ application_id }}, so every MCP client would be configured with a
bearer the endpoint rejects.
@@ -0,0 +1,22 @@
---
- name: "🩺 Ask the MCP endpoint whether the stored app password authenticates"
ansible.builtin.shell: |
# nocheck: container-exec-resolver
container exec {{ NEXTCLOUD_PROXY_LOCAL_CID | quote }} \
{{ lookup('timeout', 30) | curl }} -o /dev/null -w '%{http_code}' -X POST \
-H 'Host: {{ NEXTCLOUD_DOMAIN }}' \
-H 'Authorization: Bearer {{ nextcloud_mcp_token }}' \
-H 'Accept: application/json, text/event-stream' \
-H 'Content-Type: application/json' \
--data '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"infinito","version":"1"}}}' \
'http://127.0.0.1:{{ container_port }}{{ NEXTCLOUD_MCP_PATH }}'
args:
executable: /bin/bash
register: nextcloud_mcp_probe
retries: 30
delay: 10
until: nextcloud_mcp_probe.stdout in ['200', '401', '404']
changed_when: false
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
delegate_to: "{{ NEXTCLOUD_PROXY_HOST_NODE }}"
run_once: true
@@ -0,0 +1,84 @@
---
- name: "🔍 Read the registered AppAPI deploy daemons"
ansible.builtin.command:
argv:
- container
- exec
- -u
- "{{ NEXTCLOUD_DOCKER_USER }}"
- "{{ NEXTCLOUD_LOCAL_CID }}" # nocheck: container-exec-resolver
- php
- "{{ NEXTCLOUD_INTERNAL_OCC_COMMAND }}"
- app_api:daemon:list
register: nextcloud_appapi_daemons
changed_when: false
- name: "🚀 Register the manual-install deploy daemon"
ansible.builtin.command:
argv:
- container
- exec
- -u
- "{{ NEXTCLOUD_DOCKER_USER }}"
- "{{ NEXTCLOUD_LOCAL_CID }}" # nocheck: container-exec-resolver
- php
- "{{ NEXTCLOUD_INTERNAL_OCC_COMMAND }}"
- app_api:daemon:register
- "{{ NEXTCLOUD_CONTEXT_AGENT_DAEMON }}"
- Manual Install
- manual-install
- http
- "{{ NEXTCLOUD_CONTEXT_AGENT_DNS_NAME }}"
- "{{ NEXTCLOUD_CONTEXT_AGENT_NEXTCLOUD_URL }}"
when: NEXTCLOUD_CONTEXT_AGENT_DAEMON not in nextcloud_appapi_daemons.stdout
changed_when: true
- name: "🔍 Read the registered ExApps"
ansible.builtin.command:
argv:
- container
- exec
- -u
- "{{ NEXTCLOUD_DOCKER_USER }}"
- "{{ NEXTCLOUD_LOCAL_CID }}" # nocheck: container-exec-resolver
- php
- "{{ NEXTCLOUD_INTERNAL_OCC_COMMAND }}"
- app_api:app:list
register: nextcloud_registered_exapps
changed_when: false
- name: "🚀 Register the Context Agent ExApp with its MCP route"
ansible.builtin.command:
argv:
- container
- exec
- -u
- "{{ NEXTCLOUD_DOCKER_USER }}"
- "{{ NEXTCLOUD_LOCAL_CID }}" # nocheck: container-exec-resolver
- php
- "{{ NEXTCLOUD_INTERNAL_OCC_COMMAND }}"
- app_api:app:register
- "{{ NEXTCLOUD_CONTEXT_AGENT_SERVICE }}"
- "{{ NEXTCLOUD_CONTEXT_AGENT_DAEMON }}"
- --json-info
- "{{ NEXTCLOUD_CONTEXT_AGENT_JSON_INFO }}"
- --force-scopes
- --wait-finish
when: NEXTCLOUD_CONTEXT_AGENT_SERVICE not in nextcloud_registered_exapps.stdout
changed_when: true
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
- name: "🟢 Enable the Context Agent ExApp"
ansible.builtin.command:
argv:
- container
- exec
- -u
- "{{ NEXTCLOUD_DOCKER_USER }}"
- "{{ NEXTCLOUD_LOCAL_CID }}" # nocheck: container-exec-resolver
- php
- "{{ NEXTCLOUD_INTERNAL_OCC_COMMAND }}"
- app_api:app:enable
- "{{ NEXTCLOUD_CONTEXT_AGENT_SERVICE }}"
register: nextcloud_context_agent_enable
changed_when: "'already enabled' not in nextcloud_context_agent_enable.stdout"
@@ -0,0 +1,46 @@
---
- name: "🔑 Mint an app password for the MCP clients"
ansible.builtin.command:
argv:
- container
- exec
- -u
- "{{ NEXTCLOUD_DOCKER_USER }}"
- -e
- "NC_PASS={{ NEXTCLOUD_ADMINISTRATOR_PASSWORD }}"
- "{{ NEXTCLOUD_LOCAL_CID }}" # nocheck: container-exec-resolver
- php
- "{{ NEXTCLOUD_INTERNAL_OCC_COMMAND }}"
- user:auth-tokens:add
- "{{ NEXTCLOUD_ADMINISTRATOR_USERNAME }}"
- --password-from-env
- "--name={{ NEXTCLOUD_MCP_TOKEN_NAME }}"
register: nextcloud_mcp_token_mint
changed_when: true
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
delegate_to: "{{ NEXTCLOUD_HOST_NODE }}"
run_once: true
- name: "🔧 Take the minted app password from the command output"
ansible.builtin.set_fact:
nextcloud_mcp_token: "{{ nextcloud_mcp_token_mint.stdout_lines | last | trim }}"
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
run_once: true
- name: "🛑 Refuse an instance that issued no usable app password"
ansible.builtin.assert:
that:
- nextcloud_mcp_token | length >= 64
fail_msg: >-
occ user:auth-tokens:add returned no app password, so every MCP client
would be configured with an empty bearer and fail at first use.
- name: "💾 Persist the app password so MCP clients can read it"
ansible.builtin.include_role:
name: sys-token-store
tasks_from: write.yml
vars:
sys_token_store_user_key: administrator
sys_token_store_app: "{{ application_id }}"
sys_token_store_token: "{{ nextcloud_mcp_token }}"
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
@@ -39,7 +39,32 @@
start_period: {{ container_healthcheck_start_period }}
{% include 'roles/sys-svc-container/templates/base.yml.j2' %}
{{ lookup('container_depends_on', application_id) | indent(4) }}
{{ lookup('container_networks') | indent(4) }}
{{ lookup('container_networks', provider_self_alias=False) | indent(4) }}
{% if NEXTCLOUD_MCP_ENABLED | bool %}
{% set service_name = NEXTCLOUD_CONTEXT_AGENT_SERVICE %}
{% set container_port = NEXTCLOUD_CONTEXT_AGENT_PORT %}
{{ service_name }}:
{% include 'roles/sys-svc-container/templates/base.yml.j2' %}
{{ lookup('container_image', application_id, 'context_agent') }}
{{ lookup('compose_only', 'container_name', NEXTCLOUD_CONTEXT_AGENT_CONTAINER) }}
environment:
APP_ID: {{ NEXTCLOUD_CONTEXT_AGENT_SERVICE }}
APP_VERSION: "{{ NEXTCLOUD_CONTEXT_AGENT_VERSION }}"
APP_HOST: 0.0.0.0
APP_PORT: "{{ container_port }}"
APP_SECRET: "{{ NEXTCLOUD_CONTEXT_AGENT_SECRET }}"
NEXTCLOUD_URL: "{{ NEXTCLOUD_CONTEXT_AGENT_NEXTCLOUD_URL }}"
healthcheck:
test: ["CMD", "python3", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:{{ container_port }}/heartbeat', timeout=5)"]
interval: 30s
timeout: 10s
retries: 5
start_period: 60s
expose:
- "{{ container_port }}"
{{ lookup('container_networks', provider_self_alias=False) | indent(4) }}
{% endif %}
{% if NEXTCLOUD_HPB_SIGNALING_ENABLED | bool %}
{% set service_name = NEXTCLOUD_HPB_SERVICE %}
@@ -60,7 +85,7 @@
{% endif %}
expose:
- "{{ container_port }}"
{{ lookup('container_networks') | indent(4) }}
{{ lookup('container_networks', provider_self_alias=False) | indent(4) }}
{% else %}
network_mode: {{ NEXTCLOUD_HPB_NETWORK_MODE }}
{% endif %}
@@ -84,7 +109,7 @@
expose:
- "{{ container_port }}"
shm_size: 1g
{{ lookup('container_networks') | indent(4) }}
{{ lookup('container_networks', provider_self_alias=False) | indent(4) }}
{%+ if lookup('engine', 'redis', application_id, 'local') %}{{ lookup('depends_on', {'redis': 'service_healthy'}) }}{% endif %}
{% endif %}
@@ -108,7 +133,7 @@
retries: 10
start_period: {{ container_healthcheck_start_period }}
{{ lookup('container_depends_on', application_id) | indent(4) }}
{{ lookup('container_networks') | indent(4) }}
{{ lookup('container_networks', provider_self_alias=False) | indent(4) }}
{% if NEXTCLOUD_RECORDING_ENABLED | bool %}
{% set service_name = 'talk_recording' %}
@@ -124,7 +149,7 @@
- TZ={{ HOST_TIMEZONE }}
expose:
- "{{ NEXTCLOUD_RECORDING_PORT }}"
{{ lookup('container_networks') | indent(4) }}
{{ lookup('container_networks', provider_self_alias=False) | indent(4) }}
{{ lookup('container_volumes', application_id, service_name) | indent(4) }}
{% endif %}
@@ -25,12 +25,16 @@ MATOMO_BASE_URL={{ lookup('tls', 'web-app-matomo', 'url.base') | dotenv_quote }}
MATOMO_SERVICE_ENABLED={{ lookup('config', application_id, 'services.matomo.enabled') | string | lower }}
MCP_SERVICE_ENABLED={{ lookup('config', application_id, 'services.mcp.enabled') | string | lower }}
MOODLE_BASE_URL={{ (lookup('tls', 'web-app-moodle', 'url.base') if (lookup('config', application_id, 'services.moodle.enabled') | bool) else '') | dotenv_quote }}
NEXTCLOUD_BASE_URL={{ lookup('tls', 'web-app-nextcloud', 'url.base') | dotenv_quote }}
NEXTCLOUD_DIRECT_LOGIN_PASSWORD={{ lookup('config', application_id, 'credentials.administrator_password') | dotenv_quote }}
NEXTCLOUD_MCP_ENDPOINT_PATH={{ lookup('config', application_id, 'services.mcp.endpoint.path') | dotenv_quote }}
NEXTCLOUD_ONLYOFFICE_EXPECTED_DOCUMENT_SERVER_URL={{ NEXTCLOUD_ONLYOFFICE_URL | dotenv_quote }}
+30
View File
@@ -43,6 +43,36 @@ NEXTCLOUD_ZAMMAD_CONTAINER_ADDRESS: "{{ lookup('container_address', 'web-app-zam
NEXTCLOUD_PROXY_SERVICE: "proxy"
NEXTCLOUD_PROXY_CONTAINER: "{{ lookup('config', application_id, 'services.' ~ NEXTCLOUD_PROXY_SERVICE ~ '.name') }}"
NEXTCLOUD_PROXY_DNS_NAME: "{{ (entity_name ~ '_' ~ NEXTCLOUD_PROXY_SERVICE) if DEPLOYMENT_MODE == 'swarm' else NEXTCLOUD_PROXY_CONTAINER }}"
NEXTCLOUD_MCP_ENABLED: "{{ lookup('config', application_id, 'services.mcp.enabled') }}"
NEXTCLOUD_MCP_PATH: "{{ lookup('config', application_id, 'services.mcp.endpoint.path') }}"
NEXTCLOUD_MCP_TOKEN_NAME: "infinito-mcp"
NEXTCLOUD_CONTEXT_AGENT_SERVICE: "context_agent"
NEXTCLOUD_CONTEXT_AGENT_CONTAINER: "{{ lookup('config', application_id, 'services.' ~ NEXTCLOUD_CONTEXT_AGENT_SERVICE ~ '.name') }}"
NEXTCLOUD_CONTEXT_AGENT_VERSION: "{{ lookup('config', application_id, 'services.' ~ NEXTCLOUD_CONTEXT_AGENT_SERVICE ~ '.version') }}"
NEXTCLOUD_CONTEXT_AGENT_PORT: "{{ lookup('config', application_id, 'services.' ~ NEXTCLOUD_CONTEXT_AGENT_SERVICE ~ '.ports.internal.http') }}"
NEXTCLOUD_CONTEXT_AGENT_SECRET: "{{ lookup('config', application_id, 'credentials.context_agent_app_secret') }}"
NEXTCLOUD_CONTEXT_AGENT_DNS_NAME: "{{ (entity_name ~ '_' ~ NEXTCLOUD_CONTEXT_AGENT_SERVICE) if DEPLOYMENT_MODE == 'swarm' else NEXTCLOUD_CONTEXT_AGENT_CONTAINER }}"
NEXTCLOUD_CONTEXT_AGENT_DAEMON: "manual_install"
NEXTCLOUD_CONTEXT_AGENT_NEXTCLOUD_URL: "http://{{ NEXTCLOUD_PROXY_DNS_NAME }}:{{ container_port }}"
NEXTCLOUD_CONTEXT_AGENT_ROUTES:
- url: "^/mcp"
verb: "POST,GET,DELETE"
access_level: 1
headers_to_exclude: []
NEXTCLOUD_CONTEXT_AGENT_JSON_INFO: >-
{{
{
'id': NEXTCLOUD_CONTEXT_AGENT_SERVICE,
'name': 'Nextcloud Context Agent',
'version': NEXTCLOUD_CONTEXT_AGENT_VERSION,
'secret': NEXTCLOUD_CONTEXT_AGENT_SECRET,
'port': NEXTCLOUD_CONTEXT_AGENT_PORT | int,
'routes': NEXTCLOUD_CONTEXT_AGENT_ROUTES
} | to_json
}}
NEXTCLOUD_CRON_SERVICE: "cron"
NEXTCLOUD_CRON_CONTAINER: "{{ lookup('config', application_id, 'services.' ~ NEXTCLOUD_CRON_SERVICE ~ '.name') }}"
+6
View File
@@ -18,6 +18,12 @@ nextcloud_system_config:
- parameter: "trusted_domains 0"
value: "{{ NEXTCLOUD_DOMAIN }}"
- parameter: "trusted_domains 1"
value: "{{ NEXTCLOUD_PROXY_SERVICE }}"
- parameter: "trusted_domains 2"
value: "{{ NEXTCLOUD_PROXY_DNS_NAME }}"
- parameter: "overwrite.cli.url"
value: "{{ NEXTCLOUD_URL }}"