Repository navigation
feat(mcp): add organization-scoped MCP server listings with API, permission and launch merge (OHE-3861) - #1121
Merged
hieptl merged 3 commits intoNov 2, 2026
Conversation
…ission and launch merge (OHE-3861)
An org admin defines MCP server listings once and every member's
conversation launches with them. A listing is organization-managed
(the admin's shared credential rides inline, encrypted at rest) or
user-managed (the config references a personal secret with
${SECRET_NAME} and each member connects by creating that secret).
- org.mcp_servers (EncryptedJSON, migration 207), a store that mutates
under the org row lock, and MANAGE_ORG_MCP for owners and admins.
- GET/POST/PUT/DELETE /api/organizations/{org_id}/mcp-servers: reads
mask every credential and list the referenced secret names; writes
accept remote servers only, reject OAuth and the reserved name
"default", require organization-managed ${NAME} references to be
org-shared secrets, and refuse to delete or hand over a listing an
org Agent Profile still references.
- The resolved launch view merges the listings under the member's own
servers (a member's server keeps its name), so personal and org
Agent Profiles can reference them; plain settings loads never see
them. Conversation start skips user-managed listings whose required
secret the member has not created.
- Org Agent Profiles may reference organization-managed listings
instead of none; the MCP connection test restores a masked credential
from an organization-managed listing of the same name.
|
|
…-api-permission-and-launch
…861) Main took revision 207 for the startup title generation migration (#1119), so this branch had two 207 revisions and two alembic heads. Move the org MCP servers migration to 208 on top of it. Seed the org row in the agent profile upgrade test with a Core insert. The ORM insert writes NULL into every non-JSON org column it maps, and at the older starting revisions the new mcp_servers column does not exist yet.
Coverage reportClick to see where and how coverage changed
This report was generated by python-coverage-comment-action |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
hieptl
deleted the
hiep/ohe-3861-org-mcp-org-mcp-server-storage-api-permission-and-launch
branch
November 2, 2026 15:37
1 of 6 tasks
Contributor
|
🚀 Released in 1.83.0. |
2 of 6 tasks
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.
HUMAN:
AGENT:
Why
MCP servers exist only per member today:
OrgMember.mcp_configis the single store, the launch path only ever sees the member's own servers, and organization Agent Profiles may not reference MCP servers at all. OHE-3091 asks for organization-scoped MCP servers: an admin defines a listing once (for example a company Bifrost or a shared Datadog server) and every member's conversations get it, either with one shared credential the admin supplies (organization-managed) or with each member's own identity (user-managed, the default). This PR is the backend half; the Settings > MCP page and the Agent Profile editors follow in separate PRs behind theenable_org_mcp_serversflag from #1120.Summary
org.mcp_serversEncryptedJSON column (migration 207, nullable, no backfill) holding{servers: {<name>: {managed_by: 'org' | 'user', server: <MCPServer>}}}, a store that mutates under the org row lock, theMANAGE_ORG_MCPpermission (owner/admin), andGET/POST/PUT /{name}/DELETE /{name}under/api/organizations/{org_id}/mcp-servers. Reads mask every credential and list the${NAME}secrets each listing references, since the reference itself sits inside the masked credential. Writes accept remote servers only, reject OAuth credentials and the reserved namedefault, require an organization-managed listing's${NAME}references to be org-shared secrets (a member's personal secret of the same name would otherwise shadow them), and return 409 when deleting a listing, or handing it over to users, while an organization Agent Profile still references it. A user-managed listing stores${SECRET_NAME}where the credential goes; nothing per member is stored.SaasSettingsStore.load(resolve_agent_profile=True)) merges the listings under the member's own servers, before profile resolution, so launches without a profile get them and personal and orgmcp_server_refscan name them; a member's server keeps the bare name on a collision. Plain settings loads never see them, so a settings round-trip can never write them into a member's row. Conversation start skips a user-managed listing whose required${NAME}reference this member's secrets cannot satisfy (the sandbox keeps such a reference literally and the server would fail to authenticate). The profilematerializedry run resolves against the same map.scope=orgsaves may reference the organization-managed listings (instead of none;nullstays accepted for the legacy seededdefault), andPOST /api/v1/mcp/testrestores a redacted credential from an organization-managed listing of the same name when the caller's personal settings hold no such server.Issue Number
OHE-3861 (sub-ticket of OHE-3091)
How to Test
uv run alembic heads # 207 (head) uv run pytest tests/unit/server/routes/test_org_mcp_servers.py tests/unit/storage/test_org_mcp_servers_store.py \ tests/unit/test_org_secrets_permissions.py tests/unit/app_server/test_mcp_test_router.py \ tests/unit/app_server/test_live_status_app_conversation_service.py tests/unit/test_agent_profiles.py \ tests/unit/test_saas_settings_store.py583 tests pass (Docker is needed for the Postgres-backed store and launch tests). New coverage: store CRUD, encryption at rest and masking, the referential guard; the route permission split, validation and error mapping, and that a 422 never echoes a submitted credential; the launch merge with member-wins precedence, the plain-load isolation,
store()refusing a launch view; org-profile saves accepting organization-managed names and rejecting user-managed or unknown ones;materializeresolving an org reference; the conversation-start skip of unconnected listings on both the OpenHands and ACP paths; and the test route's org fallback.API smoke test as an admin:
POST /api/organizations/{org_id}/mcp-serverswith{"name": "bifrost-internal", "managed_by": "user", "server": {"url": "https://bifrost.acme.com/mcp", "transport": "http", "auth": {"strategy": "bearer", "value": "${BIFROST_TOKEN}"}}}, thenGETas a member returns it masked withsecret_names: ["BIFROST_TOKEN"]; a memberPOSTreturns 403. A member without a personalBIFROST_TOKENsecret starts a conversation without the server (a warning is logged), with the secret the server is attached and the reference expands in the sandbox.Video/Screenshots
None: backend only.
Type
Notes
enable_org_mcp_serversflag, as the agent-profiles API is not; the flag gates the UI only./api/v1/mcp/servers/{name}/call-tool, which keeps answering 404 for org listings.${NAME}is accepted, but an inline token is the primary path.Enterprise server image for this PR: