You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
We want external Coding Agents (Claude Code and similar MCP clients) to be able to drive NeuroWorkflow directly, so an agent loop can run against the platform from outside the browser.
The MCP server itself is already written and useful (46 tools in gui/mcp_server/workflow_mcp.py). What is missing is an authentication and authorization layer around it. Today the MCP server performs no authentication of its own — mcp = FastMCP("workflow") (gui/mcp_server/workflow_mcp.py:117) is constructed without an auth provider, and _build_headers() (workflow_mcp.py:25-47) simply forwards whatever Authorization header the caller sent on to the Django API.
That is the exact pattern the MCP specification calls "token passthrough" and forbids: an MCP server acts as an OAuth 2.1 Resource Server and must validate that a token was issued for it before doing anything with it.
Note that /mcp/ is already routed by the reverse proxy (gui/nginx/neuro-workflow-proxy.conf:29-43, gui/nginx/neuro-workflow.conf:86-97), so this is not a greenfield "add a new endpoint" task — the route exists and needs to be brought under authentication before we point external agents at it.
This issue covers the full path: close the current gap, make the server spec-compliant, tighten authorization to a level appropriate for automated callers, and document how to connect Claude Code.
Assumptions
Stated up front so they can be challenged:
Item
Assumption
Network exposure
Internet-facing over HTTPS on the existing domains. If we instead restrict to campus/VPN, Phase B discovery routing gets simpler and Phase C drops in priority.
Identity provider
Reuse the existing Keycloak realm neuroworkflow as the OAuth 2.1 authorization server. There is no PAT/API-key mechanism in the backend today, and adding one is more work and more risk than using what we already run.
Tool scope
Phase 1 ships the current read + edit tools. A workflow execution tool is deliberately deferred to Phase D, gated on a separate scope.
Phase A — Close the current gap
Small, self-contained PR. Should land before anything else.
Remove the public /mcp/ route from gui/nginx/neuro-workflow-proxy.conf and gui/nginx/neuro-workflow.conf until Phase B lands.
Safe to remove: the frontend never calls MCP directly. MCP_BASE_URL in gui/workflow_frontend/src/config/urls.ts is exported but never imported anywhere. Browser chat goes /api/chat/stream/ → Django MCPClient → http://mcp:8001 over the internal workflow Docker network, so nothing user-facing breaks.
Delete the DJANGO_API_TOKEN fallback (gui/mcp_server/workflow_mcp.py:20,46). It is not set in any compose file or .env, so it is currently dead — but "if no auth header was supplied, use a privileged token instead" is a fail-open shape we should not keep around. Return an error instead.
Add a visibility check to viewer_file (app/workflow/views.py:1063-1076, routed at config/urls.py:31). It currently has no authentication class and no visibility check, so project files are served to anyone who knows the project UUID. Path traversal is already blocked by django.views.static.serve, but private project data is not. Minimum: serve only visibility == PUBLIC. If the brain-viewer iframe genuinely needs private projects, replace with a short-lived signed URL (project UUID + expiry + HMAC).
Remove stale artifacts: gui/mcp_server/.ipynb_checkpoints/ still contains a Dockerfile referencing a proxy.py and mcp_config.json that no longer exist.
X-Internal-Secret is dead code — the frontend always sends it (src/api/authHeaders.ts:8, value from src/api/config.ts:20) and it is allow-listed in CORS_ALLOW_HEADERS (config/settings.py:168), but no middleware or view ever reads it. Remove it, or comment it as non-functional, before someone mistakes it for a security control.
Verification:POST /mcp returns 404 through the proxy; browser chat and the notebook %chat agent still work unchanged.
Phase B — Make the MCP server an OAuth 2.1 Resource Server
Goal: comply with the MCP authorization spec so that claude mcp login works out of the box via browser-based OAuth.
B-1. Dedicated Keycloak client
Add neuroworkflow-mcp to gui/keycloak/realm-export.json (today the realm has only neuroworkflow-app):
publicClient: true, standard flow, PKCE required (pkce.code.challenge.method: S256), direct access grants off
Redirect URIs http://localhost:8123/callback and http://127.0.0.1:8123/callback (fixed port, matched by --callback-port on the client side)
Audience mapper injecting neuroworkflow-mcp into aud
Client scopes workflow:read, workflow:write, and optional nodes:write (used in Phase C)
Two Keycloak-specific constraints worth recording, because both will silently break the flow otherwise:
Keycloak does not implement RFC 8707. MCP clients are required to send resource=https://<host>/mcp on both the authorization and token request, and Keycloak ignores it. The audience must therefore be injected by a hardcoded audience mapper on the client. Without this, aud validation fails every time.
Do not enable Dynamic Client Registration. Keycloak's DCR is off by default and enabling anonymous registration widens the attack surface. Use a pre-registered client instead; Claude Code supports this via --client-id / --callback-port.
B-2. Wire up RemoteAuthProvider
FastMCP v2 (already pinned at fastmcp>=2.12.4 in gui/mcp_server/Dockerfile) supports this natively. Replace workflow_mcp.py:117:
401 + WWW-Authenticate: Bearer resource_metadata="..." on unauthenticated requests
a generated /.well-known/oauth-protected-resource document (RFC 9728)
signature / iss / aud / expiry validation
Rewrite _build_headers() to read the verified token rather than blindly forwarding get_http_headers(include={"authorization"})
Add KEYCLOAK_ISSUER, MCP_PUBLIC_URL, MCP_AUDIENCE to the mcp service in gui/docker-compose.yml / docker-compose.prod.yml (note: docker-compose.prod.yml currently adds no override for mcp at all and inherits the dev definition verbatim, including the bind-mounted source file — worth fixing at the same time, related to Minimize accidental Dev/Prod configuration drift #45)
B-3. Reverse-proxy routes for discovery
Easy to miss, and the flow fails without it. Under RFC 9728, metadata for the resource https://<host>/mcp lives at https://<host>/.well-known/oauth-protected-resource/mcp. Since location / currently proxies to the frontend, that path is swallowed by the SPA and client discovery fails.
Also confirm on a real deployment that Keycloak's authorization-server metadata is reachable in the form clients probe for. Keycloak sits under the /auth/ prefix, so both /auth/realms/neuroworkflow/.well-known/oauth-authorization-server and the RFC 8414 path-insertion form /.well-known/oauth-authorization-server/auth/realms/neuroworkflow should be checked; add an nginx rewrite if the latter is required.
B-4. Accept MCP-issued tokens in Django
_verify_keycloak_client() (app/auth/authentication.py:55-77) currently accepts a token only when azp == KEYCLOAK_CLIENT_ID or KEYCLOAK_CLIENT_ID in aud. Generalize to a list, KEYCLOAK_CLIENT_IDS, mirroring how KEYCLOAK_ISSUERS already handles multiple issuers.
Add a TTL to _jwks_cache (app/auth/authentication.py:17-30). It is a module-level dict that is only refreshed on a kid miss, so a key retired during rotation stays trusted until the process restarts. ~10 minutes is fine.
On the downstream hop. Strictly, forwarding the received token onward is the shape the spec dislikes. The clean answer is Keycloak Token Exchange (RFC 8693; standard token exchange is GA in Keycloak 26.2) to swap for a neuroworkflow-app-audience token, but that needs a confidential client and more plumbing.
Proposal for this phase: have the audience mapper emit aud: ["neuroworkflow-mcp", "neuroworkflow-app"], and have the MCP server validate that the token is addressed to it before forwarding. That satisfies the core requirement — the server no longer accepts tokens that were not issued for it — and leaves Token Exchange as a follow-up once Phase C is done. Recorded as a TODO rather than silently skipped.
Verification:
# unauthenticated -> 401 with WWW-Authenticate
curl -i -X POST https://<host>/mcp -H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","method":"tools/list","id":1}'# protected resource metadata resolves
curl -s https://<host>/.well-known/oauth-protected-resource/mcp | jq
# authorization server metadata resolves
curl -s https://<host>/auth/realms/neuroworkflow/.well-known/oauth-authorization-server | jq
# a token minted for a different client is rejected
curl -i -X POST https://<host>/mcp -H "Authorization: Bearer <token with wrong aud>" ...
Backend tests to add under gui/workflow_backend/django-project/tests/: Django accepts a neuroworkflow-mcp-issued token; the JWKS cache refetches after TTL expiry.
Phase C — Authorization hardening
Phase B establishes who the caller is. It does not establish what they may do — and right now that is close to "any authenticated user can do almost anything." This needs to be tightened before we hand credentials to an automated agent.
Node files synced from disk have uploaded_by = NULL, so the first clause treats every built-in node as editable by any authenticated user. python_file_service.py:170 writes the submitted body straight to codes/nodes/<category>/<name>.py, and that directory is bind-mounted read-write into the Jupyter kernel container and sits on the kernel's PYTHONPATH (jupyterhub_config.py:41,67; gui/docker-compose.yml:21).
In other words, the node library is a write channel into the code that later executes, and modifying a trusted existing node is currently no harder than adding a new one. The corresponding MCP tools (update_python_file_code, upload_python_file, copy_python_file, bulk_sync_nodes) sit directly on top of it.
Change the rule to owner-only, with NULL (catalog-synced) files restricted to administrators
Require an explicit scope (nodes:write, below) on the MCP side for all four tools
This one should land regardless of whether we proceed with external exposure.
C-2. Scope separation
Define as Keycloak client scopes, enforce via JWTVerifier(required_scopes=...) plus per-tool checks:
Scope
Tools
workflow:read
list_projects, get_flow, get_node, list_edges, get_workflow_facts, read-side viewer_*, health
upload_python_file, update_python_file_code, copy_python_file, bulk_sync_nodes — effectively code-execution authority, see C-1
Grant workflow:read + workflow:write by default; make nodes:write an optional scope requiring explicit consent
On insufficient scope, return 403 with WWW-Authenticate: Bearer error="insufficient_scope", scope="nodes:write" so clients can perform step-up authorization (Claude Code handles this automatically)
Related consideration: app/workflow/permissions.py makes public projects writable by any authenticated user (get_accessible_project(..., write=True) returns the project when visibility == PUBLIC; only DELETE and visibility changes are owner-only). That is an intentional product decision for humans in the GUI, but it reads differently once automated agents are calling the same API. Worth an explicit decision rather than inheriting it by default.
C-3. Rate limiting
There is none anywhere today: no DEFAULT_THROTTLE_CLASSES in config/settings.py:121-134, no limit_req in either nginx config.
nginx limit_req on /mcp/ and /api/chat/
DRF throttles, with tighter limits on app/chat/* since those calls bill against the org's OpenAI/Anthropic keys
C-4. Audit logging
Emit structured logs from the MCP server: verified token sub / client_id, tool name, target project ID. If agents are writing autonomously, we need to be able to reconstruct what happened after the fact.
Phase D — Claude Code connectivity
D-1. Connection procedure
claude mcp add --transport http \
--client-id neuroworkflow-mcp \
--callback-port 8123 \
--scope user \
neuroworkflow https://<host>/mcp
claude mcp login neuroworkflow # browser-based Keycloak login
claude mcp login neuroworkflow --no-browser for headless/SSH environments (prints the authorization URL, accepts the redirect URL pasted back; needs ssh -t)
claude mcp list shows ✔ Connected / ! Needs authentication
Tokens are stored by Claude Code and refreshed automatically; claude mcp logout neuroworkflow clears them
For CI or non-interactive use, headersHelper generates headers at connection time so no secret is written into the config file:
NEUROWORKFLOW_SERVICE_TOKEN is no longer the same value as the JupyterHub admin token. Today JUPYTERHUB_API_TOKEN serves as the JupyterHub admin API token, the credential for AnthropicProxyView (app/chat/views.py:274-352), and an environment variable readable from inside every user kernel. docs/NOTEBOOK_CHAT_AGENT.md:211 accepts this explicitly as a trusted-lab tradeoff, which is a reasonable call for the current deployment but should be revisited before external agents are in scope.
Items 3 and 4 are separable work. If we want to ship MCP execution sooner, the alternative is to document explicitly that MCP-driven execution is limited to trusted users within the lab.
D-3. Documentation
New docs/MCP_REMOTE_ACCESS.md: architecture, Keycloak client setup, claude mcp add walkthrough, scope table, troubleshooting
docs/ARCHITECTURE_FULL.md §3.4/§4.8/§5 and docs/ARCHITECTURE_DIAGRAM.svg still describe a proxy.py + mcp_config.json entry point and a utils/mcp/ directory, none of which exist. The actual entry point is workflow_mcp.py, configured via MCP_PORT + DJANGO_API_URL, authenticated by Keycloak.
End-to-end check once Phase B lands: claude mcp add ... && claude mcp login neuroworkflow → claude mcp list reports ✔ Connected → asking Claude Code to list projects results in an actual tool call.
Note for whoever runs the backend tests: /app inside the backend container is a stale build-time snapshot. The live code is the /django-app bind mount, which is also the working directory. Don't cd /app.
Open questions
Network exposure — internet-facing, or restrict to campus/VPN? Restricting simplifies Phase B and de-prioritizes Phase C, at the cost of ruling out external collaborators and cloud-hosted agents.
Execution tool (D-2) — add it, and if so, which prerequisites do we clear first?
Token Exchange — migrate to RFC 8693 after Phase C, or keep the dual-audience approach?
Shared service token — out of scope here, but JUPYTERHUB_API_TOKEN doing three jobs at once deserves its own issue.
Related: #28 (per-user execution isolation, prerequisite for D-2), #45 (dev/prod configuration drift, overlaps with the compose changes in B-2).
Summary
We want external Coding Agents (Claude Code and similar MCP clients) to be able to drive NeuroWorkflow directly, so an agent loop can run against the platform from outside the browser.
The MCP server itself is already written and useful (46 tools in
gui/mcp_server/workflow_mcp.py). What is missing is an authentication and authorization layer around it. Today the MCP server performs no authentication of its own —mcp = FastMCP("workflow")(gui/mcp_server/workflow_mcp.py:117) is constructed without an auth provider, and_build_headers()(workflow_mcp.py:25-47) simply forwards whateverAuthorizationheader the caller sent on to the Django API.That is the exact pattern the MCP specification calls "token passthrough" and forbids: an MCP server acts as an OAuth 2.1 Resource Server and must validate that a token was issued for it before doing anything with it.
Note that
/mcp/is already routed by the reverse proxy (gui/nginx/neuro-workflow-proxy.conf:29-43,gui/nginx/neuro-workflow.conf:86-97), so this is not a greenfield "add a new endpoint" task — the route exists and needs to be brought under authentication before we point external agents at it.This issue covers the full path: close the current gap, make the server spec-compliant, tighten authorization to a level appropriate for automated callers, and document how to connect Claude Code.
Assumptions
Stated up front so they can be challenged:
neuroworkflowas the OAuth 2.1 authorization server. There is no PAT/API-key mechanism in the backend today, and adding one is more work and more risk than using what we already run.Phase A — Close the current gap
Small, self-contained PR. Should land before anything else.
/mcp/route fromgui/nginx/neuro-workflow-proxy.confandgui/nginx/neuro-workflow.confuntil Phase B lands.Safe to remove: the frontend never calls MCP directly.
MCP_BASE_URLingui/workflow_frontend/src/config/urls.tsis exported but never imported anywhere. Browser chat goes/api/chat/stream/→ DjangoMCPClient→http://mcp:8001over the internalworkflowDocker network, so nothing user-facing breaks.DJANGO_API_TOKENfallback (gui/mcp_server/workflow_mcp.py:20,46). It is not set in any compose file or.env, so it is currently dead — but "if no auth header was supplied, use a privileged token instead" is a fail-open shape we should not keep around. Return an error instead.viewer_file(app/workflow/views.py:1063-1076, routed atconfig/urls.py:31). It currently has no authentication class and no visibility check, so project files are served to anyone who knows the project UUID. Path traversal is already blocked bydjango.views.static.serve, but private project data is not. Minimum: serve onlyvisibility == PUBLIC. If the brain-viewer iframe genuinely needs private projects, replace with a short-lived signed URL (project UUID + expiry + HMAC).gui/mcp_server/.ipynb_checkpoints/still contains a Dockerfile referencing aproxy.pyandmcp_config.jsonthat no longer exist.X-Internal-Secretis dead code — the frontend always sends it (src/api/authHeaders.ts:8, value fromsrc/api/config.ts:20) and it is allow-listed inCORS_ALLOW_HEADERS(config/settings.py:168), but no middleware or view ever reads it. Remove it, or comment it as non-functional, before someone mistakes it for a security control.Verification:
POST /mcpreturns 404 through the proxy; browser chat and the notebook%chatagent still work unchanged.Phase B — Make the MCP server an OAuth 2.1 Resource Server
Goal: comply with the MCP authorization spec so that
claude mcp loginworks out of the box via browser-based OAuth.B-1. Dedicated Keycloak client
Add
neuroworkflow-mcptogui/keycloak/realm-export.json(today the realm has onlyneuroworkflow-app):publicClient: true, standard flow, PKCE required (pkce.code.challenge.method: S256), direct access grants offhttp://localhost:8123/callbackandhttp://127.0.0.1:8123/callback(fixed port, matched by--callback-porton the client side)neuroworkflow-mcpintoaudworkflow:read,workflow:write, and optionalnodes:write(used in Phase C)Two Keycloak-specific constraints worth recording, because both will silently break the flow otherwise:
resource=https://<host>/mcpon both the authorization and token request, and Keycloak ignores it. The audience must therefore be injected by a hardcoded audience mapper on the client. Without this,audvalidation fails every time.--client-id/--callback-port.B-2. Wire up
RemoteAuthProviderFastMCP v2 (already pinned at
fastmcp>=2.12.4ingui/mcp_server/Dockerfile) supports this natively. Replaceworkflow_mcp.py:117:This gives us, for free:
401+WWW-Authenticate: Bearer resource_metadata="..."on unauthenticated requestsa generated
/.well-known/oauth-protected-resourcedocument (RFC 9728)signature /
iss/aud/ expiry validationRewrite
_build_headers()to read the verified token rather than blindly forwardingget_http_headers(include={"authorization"})Add
KEYCLOAK_ISSUER,MCP_PUBLIC_URL,MCP_AUDIENCEto themcpservice ingui/docker-compose.yml/docker-compose.prod.yml(note:docker-compose.prod.ymlcurrently adds no override formcpat all and inherits the dev definition verbatim, including the bind-mounted source file — worth fixing at the same time, related to Minimize accidental Dev/Prod configuration drift #45)B-3. Reverse-proxy routes for discovery
Easy to miss, and the flow fails without it. Under RFC 9728, metadata for the resource
https://<host>/mcplives athttps://<host>/.well-known/oauth-protected-resource/mcp. Sincelocation /currently proxies to the frontend, that path is swallowed by the SPA and client discovery fails./auth/prefix, so both/auth/realms/neuroworkflow/.well-known/oauth-authorization-serverand the RFC 8414 path-insertion form/.well-known/oauth-authorization-server/auth/realms/neuroworkflowshould be checked; add an nginx rewrite if the latter is required.B-4. Accept MCP-issued tokens in Django
_verify_keycloak_client()(app/auth/authentication.py:55-77) currently accepts a token only whenazp == KEYCLOAK_CLIENT_IDorKEYCLOAK_CLIENT_ID in aud. Generalize to a list,KEYCLOAK_CLIENT_IDS, mirroring howKEYCLOAK_ISSUERSalready handles multiple issuers._jwks_cache(app/auth/authentication.py:17-30). It is a module-level dict that is only refreshed on akidmiss, so a key retired during rotation stays trusted until the process restarts. ~10 minutes is fine.On the downstream hop. Strictly, forwarding the received token onward is the shape the spec dislikes. The clean answer is Keycloak Token Exchange (RFC 8693; standard token exchange is GA in Keycloak 26.2) to swap for a
neuroworkflow-app-audience token, but that needs a confidential client and more plumbing.Proposal for this phase: have the audience mapper emit
aud: ["neuroworkflow-mcp", "neuroworkflow-app"], and have the MCP server validate that the token is addressed to it before forwarding. That satisfies the core requirement — the server no longer accepts tokens that were not issued for it — and leaves Token Exchange as a follow-up once Phase C is done. Recorded as a TODO rather than silently skipped.Verification:
Backend tests to add under
gui/workflow_backend/django-project/tests/: Django accepts aneuroworkflow-mcp-issued token; the JWKS cache refetches after TTL expiry.Phase C — Authorization hardening
Phase B establishes who the caller is. It does not establish what they may do — and right now that is close to "any authenticated user can do almost anything." This needs to be tightened before we hand credentials to an automated agent.
C-1. Node code library ownership check (highest priority)
app/box/views.py:27:Node files synced from disk have
uploaded_by = NULL, so the first clause treats every built-in node as editable by any authenticated user.python_file_service.py:170writes the submitted body straight tocodes/nodes/<category>/<name>.py, and that directory is bind-mounted read-write into the Jupyter kernel container and sits on the kernel'sPYTHONPATH(jupyterhub_config.py:41,67;gui/docker-compose.yml:21).In other words, the node library is a write channel into the code that later executes, and modifying a trusted existing node is currently no harder than adding a new one. The corresponding MCP tools (
update_python_file_code,upload_python_file,copy_python_file,bulk_sync_nodes) sit directly on top of it.NULL(catalog-synced) files restricted to administratorsnodes:write, below) on the MCP side for all four toolsThis one should land regardless of whether we proceed with external exposure.
C-2. Scope separation
Define as Keycloak client scopes, enforce via
JWTVerifier(required_scopes=...)plus per-tool checks:workflow:readlist_projects,get_flow,get_node,list_edges,get_workflow_facts, read-sideviewer_*,healthworkflow:writecreate_project,update_flow,add_node,update_node*,delete_node,add_edge,delete_edge,save_report,generate_code_batchnodes:writeupload_python_file,update_python_file_code,copy_python_file,bulk_sync_nodes— effectively code-execution authority, see C-1workflow:read+workflow:writeby default; makenodes:writean optional scope requiring explicit consent403withWWW-Authenticate: Bearer error="insufficient_scope", scope="nodes:write"so clients can perform step-up authorization (Claude Code handles this automatically)Related consideration:
app/workflow/permissions.pymakes public projects writable by any authenticated user (get_accessible_project(..., write=True)returns the project whenvisibility == PUBLIC; only DELETE and visibility changes are owner-only). That is an intentional product decision for humans in the GUI, but it reads differently once automated agents are calling the same API. Worth an explicit decision rather than inheriting it by default.C-3. Rate limiting
There is none anywhere today: no
DEFAULT_THROTTLE_CLASSESinconfig/settings.py:121-134, nolimit_reqin either nginx config.limit_reqon/mcp/and/api/chat/app/chat/*since those calls bill against the org's OpenAI/Anthropic keysC-4. Audit logging
sub/client_id, tool name, target project ID. If agents are writing autonomously, we need to be able to reconstruct what happened after the fact.Phase D — Claude Code connectivity
D-1. Connection procedure
claude mcp login neuroworkflow --no-browserfor headless/SSH environments (prints the authorization URL, accepts the redirect URL pasted back; needsssh -t)claude mcp listshows✔ Connected/! Needs authenticationclaude mcp logout neuroworkflowclears themFor CI or non-interactive use,
headersHelpergenerates headers at connection time so no secret is written into the config file:{ "mcpServers": { "neuroworkflow": { "type": "http", "url": "https://<host>/mcp", "headersHelper": "/opt/bin/neuroworkflow-token.sh" }}}(That script would use a client-credentials grant against a separate confidential Keycloak client, subject to the same scope model as C-2.)
D-2. Whether to add an execution tool — decision needed
There is currently no
run_workflowMCP tool. Fully realizing "run an agent loop from outside" needs one, wrappingPOST /api/workflow/{id}/run/.Recommend holding off until the following are true, because workflow execution currently runs in a shared context:
execute:workflowexists as its own scope and is not granted by defaultJUPYTER_EXECUTION_USERdefaults to a single shared account (user1), so every user's workflow runs in the same kernel container, which is spawned withGRANT_SUDO=yes. This overlaps with JupyterLab project files are not isolated per user or named per project (incl. output folder) #28.NEUROWORKFLOW_SERVICE_TOKENis no longer the same value as the JupyterHub admin token. TodayJUPYTERHUB_API_TOKENserves as the JupyterHub admin API token, the credential forAnthropicProxyView(app/chat/views.py:274-352), and an environment variable readable from inside every user kernel.docs/NOTEBOOK_CHAT_AGENT.md:211accepts this explicitly as a trusted-lab tradeoff, which is a reasonable call for the current deployment but should be revisited before external agents are in scope.Items 3 and 4 are separable work. If we want to ship MCP execution sooner, the alternative is to document explicitly that MCP-driven execution is limited to trusted users within the lab.
D-3. Documentation
docs/MCP_REMOTE_ACCESS.md: architecture, Keycloak client setup,claude mcp addwalkthrough, scope table, troubleshootingdocs/ARCHITECTURE_FULL.md§3.4/§4.8/§5 anddocs/ARCHITECTURE_DIAGRAM.svgstill describe aproxy.py+mcp_config.jsonentry point and autils/mcp/directory, none of which exist. The actual entry point isworkflow_mcp.py, configured viaMCP_PORT+DJANGO_API_URL, authenticated by Keycloak.CLAUDE.md:134-135Files touched
gui/nginx/neuro-workflow-proxy.conf,neuro-workflow.confgui/mcp_server/workflow_mcp.pygui/keycloak/realm-export.jsongui/workflow_backend/django-project/app/auth/authentication.pygui/workflow_backend/django-project/app/box/views.pygui/workflow_backend/django-project/app/workflow/views.pygui/workflow_backend/django-project/config/settings.pygui/docker-compose.yml,docker-compose.prod.ymldocs/MCP_REMOTE_ACCESS.md(new),docs/ARCHITECTURE_FULL.md,CLAUDE.mdEnd-to-end check once Phase B lands:
claude mcp add ... && claude mcp login neuroworkflow→claude mcp listreports✔ Connected→ asking Claude Code to list projects results in an actual tool call.Open questions
JUPYTERHUB_API_TOKENdoing three jobs at once deserves its own issue.Related: #28 (per-user execution isolation, prerequisite for D-2), #45 (dev/prod configuration drift, overlaps with the compose changes in B-2).