Repository navigation
Add multi-user mode: a Nextcloud login per HTTP request - #87
Open
BjoernSchotte wants to merge 1 commit into
Open
BjoernSchotte wants to merge 1 commit into
BjoernSchotte wants to merge 1 commit into
Conversation
NEXTCLOUD_MCP_MULTIUSER=true lets one HTTP server act for many Nextcloud accounts. The server holds no credentials of its own. An ASGI middleware requires "Authorization: Basic <login:app-password>" on every request, resolves a new login to its user ID once (GET /ocs/v2.php/cloud/user, which also checks the credentials) and pins that login's client to the request. get_client() and get_config() return the request's client and config, so no tool had to change. - No header, a malformed or repeated one, or a login that Nextcloud rejects gives 401; Nextcloud unreachable gives 502. There is no fallback account. - Discovery without a login: a POST without Authorization header whose JSON-RPC messages are all initialize, ping, tools/list or notifications runs without a Nextcloud client. tools/call still needs a login, and a wrong header is never downgraded to discovery. - Clients are cached per login (LRU, NEXTCLOUD_MCP_MULTIUSER_CACHE_SIZE, idle TTL NEXTCLOUD_MCP_MULTIUSER_TTL). The cache key is an HMAC of login and password with a per-process random key. Lifespan shutdown closes all cached and retired clients. - X-Nextcloud-MCP-Permissions lowers, never raises, the level per request. - Basic Auth now sends the login name when it differs from the user ID (login by e-mail address); WebDAV paths keep using the user ID. Without NEXTCLOUD_MCP_MULTIUSER nothing changes: stdio and HTTP still use NEXTCLOUD_USER and NEXTCLOUD_PASSWORD. Tests: unit tests for header parsing, config, the client pool and the middleware over the real Streamable HTTP app (parallel logins never swap); integration tests with two users, one logging in by e-mail address. Signed-off-by: Björn Schotte <bjoern.schotte@googlemail.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Important
This repository does not receive automatic reviews because it has fewer than 10 stars. ⚙️ Run configuration
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Motivation
Today one server process acts as one Nextcloud account (
NEXTCLOUD_USER/NEXTCLOUD_PASSWORD). A team thatwants to give several people an MCP server must run one process per person, or share one account and lose
Nextcloud's own permissions, audit trail and per-user data.
This PR adds an opt-in multi-user mode for the HTTP transport: one server, no stored credentials, and every
request brings its own Nextcloud login. Each user's tools then run with that user's own Nextcloud rights.
Design
NEXTCLOUD_MCP_MULTIUSER=true(needs--transport http).NEXTCLOUD_USERandNEXTCLOUD_PASSWORDmust notbe set, so there is no account to fall back to.
multiuser.py) wraps the Streamable HTTP app. It readsAuthorization: Basic <login:app-password>, resolves a new login once to its user ID withGET /ocs/v2.php/cloud/user(this also checks the credentials) and pins that login's client and config tothe request.
get_client()andget_config()return the client and config of the running request. No tool had tochange. The binding is read from the request scope that the MCP layer passes into the tool call, with a
context variable as fallback.
login name (
Config.auth_login), and WebDAV paths keep using the user ID. In single-user mode both are thesame value.
NextcloudClientper login, LRU (NEXTCLOUD_MCP_MULTIUSER_CACHE_SIZE, default 64) withan idle TTL (
NEXTCLOUD_MCP_MULTIUSER_TTL, default 900 s). Two logins never share a client. Concurrent firstrequests of one login share one lookup. A client that is dropped from the pool is closed after a grace
period, so a running tool call can finish. On lifespan shutdown all clients are closed.
X-Nextcloud-MCP-Permissions: read|write|destructivecan lower, but neverraise, the server's
NEXTCLOUD_MCP_PERMISSIONSfor that request (for example a read-only service accountbehind the same server). This plugs into
permissions.get_permission_level()through a small provider hook.initializeandtools/listat startup, before theyknow which user will call. A
POSTwithoutAuthorizationheader is allowed only if all of its JSON-RPCmessages are
initialize,ping,tools/list,notifications/initializedornotifications/cancelled.The answer is the static tool list, and no Nextcloud client is created.
HTTP semantics
Authorizationheader (except discovery), malformed or repeated header, or a login that Nextcloud rejects (401/403)401+WWW-Authenticate: BasicX-Nextcloud-MCP-Permissions400cloud/user502GET/DELETEwithout a login405(Allow: POST)Security considerations
NEXTCLOUD_USER/NEXTCLOUD_PASSWORDin this mode.comes only from that request's header. A test runs parallel requests of two logins and checks that they
never swap clients.
Authorizationheader is refused. It is never downgraded to unauthenticated discovery. Thediscovery body is read with a 64 KiB limit before the check; larger bodies get
401.password nor a plain hash of it is kept as a dict key.
Credentials.__repr__hides the password.A changed app password gives a new client. A revoked app password fails at once: Nextcloud refuses each
call of the cached client (a tool error). The middleware itself answers
401once that client was idlefor the TTL and is looked up again.
Authorizationvalues with control characters or over 4 KiB are rejected.NEXTCLOUD_MCP_UPLOAD_ROOTis refused in this mode, because it would give every user access to theserver's local files.
between client and server.
Configuration
Documented in the README under "Multi-User Mode (HTTP only)":
Tests
tests/test_multiuser.py(unit tests, 59): header parsing, permission caps, config validation, client pool(LRU, TTL, password change, rejected logins not cached, shared first lookup, close of cached and retired
clients), state binding, and the middleware over the real Streamable HTTP app (401/400/502/405, permission
cap, parallel logins never swap, discovery allowed and refused cases, lifespan shutdown closes the pool).
tests/integration/test_multiuser.py(integration): two real users with app passwords from Nextcloud, onelogging in by e-mail address, parallel calls, 401 cases and the read-only cap.
ruff check,ruff format --check,isort --check src/,pyrightare clean, and756 unit tests pass on Python 3.12. The integration tests were not run locally, because there was no
Nextcloud test instance.
PROGRESS.mdgets a "Multi-User" row, recounted withscripts/sync_progress.py --write.Backwards compatibility
The default is unchanged. Without
NEXTCLOUD_MCP_MULTIUSER, stdio and HTTP still useNEXTCLOUD_USERandNEXTCLOUD_PASSWORD, andcreate_server()creates the global client as before.create_server(config)and thenew
create_http_app(mcp, config)are the only changed entry points;__main__only uses uvicorn directly whenmulti-user mode is on. The
NEXTCLOUD_MCP_APP_PASSWORDparsing moved into a shared helper with the sameaccepted values and error message format.
AI disclosure: this change was written with Claude Code (Claude Opus 5.5); commits carry a Co-Authored-By trailer.
🤖 Generated with Claude Code