# Managed MCP Architecture ## Purpose Project David treats MCP as a managed extension of the existing assistant tool system. The design does not expose a remote MCP server directly to an assistant. Instead, Project David owns the registration, discovery, naming, attachment, routing, execution lifecycle, and persistence boundaries around the remote server. The result is a layered model: ```text Remote MCP server | | MCP protocol v Project David transport | | discovery adaptation v Project David tool identity | | explicit assistant attachment v Assistant tool configuration | | runtime binding v Project David Tool ABI | | tools/call v Remote MCP server ``` The central architectural rule is: > MCP supplies remote capabilities. Project David remains the authority for assistant ownership, capability attachment, tool naming, Run and Action lifecycle, cancellation, persistence, and model-facing tool configuration. --- ## Architecture at a glance ```text MANAGEMENT PLANE ================ Client / SDK | | authenticated API v +---------------------------+ | routers/mcp_router.py | +-------------+-------------+ | v +-------------------------------+ | McpRegistrationService | | services/ | | mcp_registration_service.py | +----+----------------------+---+ | | | | v v McpServerRegistration AssistantMcpTool | | | | +----------+-----------+ | v Assistant.tool_configs | | cache invalidation v assistant runtime state DISCOVERY PLANE =============== McpRegistrationService | v +----------------------------+ | RemoteMcpClient | | mcp_remote_client.py | +-------------+--------------+ | | MCP tools/list v Remote MCP server | | official MCP SDK types v +----------------------------+ | mcp_tool_discovery.py | | | | remote_name | | canonical_id | | provider_name | | ToolDefinition | +-------------+--------------+ | v Project David tool model EXECUTION PLANE =============== Model emits provider_name | v +---------------------------------------+ | ConsumerToolHandlersMixin | | consumer_tool_handlers_mixin.py | +-------------------+-------------------+ | | ephemeral lookup v McpToolExecutor | | ToolCallEnvelope v +--------------------------------+ | mcp_tool_executor.py | +---------------+----------------+ | | remote_name + arguments v +--------------------------------+ | RemoteMcpClient | +---------------+----------------+ | | MCP tools/call v Remote MCP server | | CallToolResult v ToolResultEnvelope | v Project David lifecycle ``` --- # 1. Managed server registration A remote MCP endpoint first becomes a Project David managed registration. The registration service owns: - URL validation and normalization - user ownership - stable registration identity - transport identity - timeout configuration - enabled/disabled state - CRUD lifecycle The canonical service is: ```text src/api/entities_api/services/mcp_registration_service.py ``` Registration identity is not based on the mutable display name. The service normalizes the endpoint URL, then derives an identity key from: ```text transport + normalized_url ``` Conceptually: ```text user | +-- MCP registration | +-- id stable Project David registration id +-- name mutable display / namespace name +-- normalized_url +-- identity_key +-- transport +-- timeout_seconds +-- enabled ``` The persistent registration ID is deliberately used as the stable server identity during discovery. This matters because a display name can change without changing the identity of the remote server registration. --- # 2. Registration ownership boundary MCP registrations are user-owned resources. The management service resolves registrations with both: ```text server_id owner_id ``` An assistant is also ownership checked before MCP tools can be attached or detached. This means an MCP endpoint is not a global capability merely because it exists in the database. The authenticated user must own the registration and must be allowed to modify the target assistant. Disabled registrations remain persisted, but discovery and attachment paths can require the registration to be enabled. ```text Authenticated user | +---- owns ----> McpServerRegistration | +---- may modify ----> Assistant | +---- MCP attachments ``` --- # 3. Discovery is not attachment Discovery and attachment are separate operations. Discovery asks: ```text What tools does this registered remote MCP server currently advertise? ``` Attachment asks: ```text Which of those tools should this assistant actually receive? ``` They are intentionally not the same operation. A single MCP server may expose many unrelated capabilities. Project David therefore does not assume that every tool advertised by one server should be attached to an assistant. ```text Remote server | | tools/list v +----------------------+ | discovered tools | | | | search.issues | | get.issue | | create.issue | | delete.repository | +----------+-----------+ | | explicit selection v +----------------------+ | assistant attachment | | | | search.issues | | get.issue | +----------------------+ ``` The Core discovery endpoint returns discovery data. The SDK adds ergonomic collection and selection behavior on top of this boundary. --- # 4. Remote transport boundary The remote protocol boundary lives in: ```text src/api/entities_api/orchestration/mcp_remote_client.py ``` `RemoteMcpClient` is built on the official MCP Python SDK and Streamable HTTP transport. Its responsibilities are intentionally narrow: ```text connect initialize MCP session expose negotiated server metadata tools/list tools/call transport lifetime ``` It is not responsible for: ```text assistant policy tool persistence provider alias generation assistant attachment Run lifecycle Action lifecycle routing policy ``` The client validates remote URLs as absolute HTTP or HTTPS endpoints. Authentication headers, TLS customization, and similar transport configuration can be supplied through a caller-owned HTTP client. The remote client itself does not need to become the persistence layer for raw credentials. This separation keeps protocol transport independent from Project David capability policy. --- # 5. Tool discovery adaptation Remote MCP tools use MCP-native names and schemas. Project David adapts them before they enter the internal tool system. The adapter lives in: ```text src/api/entities_api/orchestration/mcp_tool_discovery.py ``` The important internal representation is `McpDiscoveredTool`. Conceptually: ```text MCP Tool | | name | description | inputSchema v +--------------------------------+ | McpDiscoveredTool | | | | server_id | | remote_name | | canonical_id | | provider_name | | definition | +--------------------------------+ ``` This creates four distinct identity concepts. ## `server_id` The stable Project David registration ID for the remote MCP server. ## `remote_name` The name advertised by the remote MCP server. Example: ```text search.issues ``` This is the name sent back to the remote server during `tools/call`. ## `canonical_id` A stable Project David identity for the logical tool. It separates tool identity from the provider-facing alias used by the model. ## `provider_name` The name exposed inside Project David's model-facing function tool namespace. Example shape: ```text github__search_issues ``` The discovery adapter keeps readable aliases where possible, but can add deterministic disambiguation when normalization would collide, a name is already reserved, or the provider-facing name would exceed its constraints. ## `definition` The Project David tool definition derived from the MCP schema. This is what eventually becomes part of the assistant's model-facing tool configuration. --- # 6. Why `remote_name` and `provider_name` are different The model does not need to see the remote MCP name directly. The remote server does not need to know Project David's provider alias. ```text Model-facing namespace Remote MCP namespace github__search_issues -----------> search.issues ^ | provider_name remote_name ``` This gives Project David a controlled namespace where MCP tools can coexist with: - built-in tools - developer-defined function tools - other MCP servers - existing orchestration tools It also allows Project David to reject alias collisions before execution. --- # 7. Assistant attachment is durable provenance Attaching a discovered MCP tool does two related things. First, it creates or updates durable MCP provenance: ```text AssistantMcpTool ``` Second, it injects the corresponding function definition into: ```text Assistant.tool_configs ``` The service coordinating both sides is: ```text src/api/entities_api/services/mcp_registration_service.py ``` The helper functions that keep `tool_configs` coherent live in: ```text src/api/entities_api/services/mcp_tool_config.py ``` The relationship is: ```text McpServerRegistration | | registration_id v AssistantMcpTool | | assistant_id v Assistant | +---- tool_configs ``` An `AssistantMcpTool` row carries durable provenance such as: ```text assistant_id registration_id remote_name canonical_id provider_name enabled ``` The assistant's `tool_configs` holds the model-facing function definition. These two representations serve different purposes: ```text AssistantMcpTool = provenance and durable MCP attachment identity Assistant.tool_configs = model-facing capability schema ``` They must remain coherent. --- # 8. Provider aliases are durable once attached Discovery may calculate a provider alias for a tool. Once an MCP tool has been attached to an assistant, the persisted provider alias is treated as durable for that attachment. If discovery metadata is later refreshed, Core updates the tool definition while retaining the existing attachment alias. This prevents a tool from unexpectedly changing its model-facing identity during normal metadata refresh. ```text First attachment remote_name: search.issues provider_name: github__search_issues Later discovery refresh remote_name: search.issues new metadata: ... persisted alias: github__search_issues ``` This stability is important because tool names participate in model output, routing, persisted actions, and debugging. --- # 9. Attachment validates current discovery Attachment does not blindly persist arbitrary remote tool names. Before attachment, Core discovers the server's current tools and verifies that every requested remote name is actually advertised. ```text requested names | v discover current server tools | v +---------------------------+ | requested subset exists? | +-------------+-------------+ | +-----+-----+ | | yes no | | v v attach reject ``` This keeps the management plane tied to current remote capability state. The service also handles concurrent attachment conflicts and protects provider alias uniqueness. --- # 10. Detachment removes both capability and provenance Detachment reverses the assistant attachment. Core removes: ```text AssistantMcpTool rows ``` and removes the associated provider-facing function definitions from: ```text Assistant.tool_configs ``` Detaching an already detached tool is treated as a no-op. The operation is assistant-specific. It does not delete the underlying server registration. ```text delete registration != detach tool from assistant ``` Deleting a registration is broader because all assistant attachments associated with that registration must be removed from the affected assistants. --- # 11. Assistant cache coherence MCP attachment state affects the assistant's effective tool configuration. After MCP attachment, detachment, registration deletion, or relevant registration changes, Core invalidates the assistant cache. The MCP registration service obtains the synchronous invalidator through: ```text src/api/entities_api/utilities/cache_utils.py ``` Conceptually: ```text MCP state mutation | v database commit | v assistant cache invalidation | v next assistant load sees current tool state ``` Cache invalidation failure is logged, while the durable database mutation remains the source of truth. --- # 12. Assistant service interaction The assistant service is MCP-aware. Relevant Core file: ```text src/api/entities_api/services/assistants_service.py ``` It imports `AssistantMcpTool` and uses enabled MCP attachment provider names when working with assistant tool configuration. This is one of the integration points between the generic assistant system and the MCP management plane. MCP therefore extends the assistant capability model rather than replacing it. --- # 13. Internal Tool ABI MCP execution does not bypass Project David's existing orchestration model. The common execution boundary lives in: ```text src/api/entities_api/orchestration/tool_abi.py ``` Important abstractions include: ```text ToolCallEnvelope ToolResultEnvelope ToolExecutor ``` The call envelope carries orchestration identity alongside tool arguments: ```text name arguments run_id thread_id assistant_id tool_call_id ``` Conceptually: ```text provider-specific call | v +------------------+ | ToolCallEnvelope | +--------+---------+ | v ToolExecutor | v +--------------------+ | ToolResultEnvelope | +--------------------+ ``` The ABI prevents MCP protocol details from leaking through the whole orchestration stack. --- # 14. MCP execution adapter Remote MCP execution is owned by: ```text src/api/entities_api/orchestration/mcp_tool_executor.py ``` `McpToolExecutor` binds: ```text one discovered tool + one RemoteMcpClient factory ``` The executor accepts a `ToolCallEnvelope`. It verifies that the incoming model-facing name matches the discovered tool's `provider_name`, then translates execution back to the remote MCP namespace: ```text ToolCallEnvelope.name | | provider_name v McpToolExecutor | | remote_name v RemoteMcpClient.call_tool(...) ``` Example: ```text model emits: github__search_issues executor maps to: search.issues remote call: tools/call("search.issues", arguments) ``` Transport or protocol failures are converted into tool failures rather than escaping uncontrolled into the orchestration loop. --- # 15. Runtime executor bindings are ephemeral Durable MCP attachment and runtime execution binding are intentionally different concepts. The persisted attachment lives in the database: ```text AssistantMcpTool ``` The runtime routing registry lives on an orchestrator instance: ```text _mcp_tool_executors ``` The registry is managed in: ```text src/api/entities_api/orchestration/mixins/consumer_tool_handlers_mixin.py ``` The mixin exposes: ```text bind_mcp_tool_executor(...) unbind_mcp_tool_executor(...) _get_mcp_tool_executor(...) ``` and keys the registry by `provider_name`. ```text Durable state Runtime state AssistantMcpTool orchestrator instance | | | provider_name | +------------------------------> _mcp_tool_executors | +-- alias -> McpToolExecutor ``` The runtime registry also rejects collisions with reserved internal routed tool names. The source material reviewed for this document shows the binding registry and its execution behavior. It does not include the higher-level composition site that reconstructs those ephemeral bindings from durable attachment records, so that composition step is deliberately not assigned to a specific file here. --- # 16. Tool routing MCP tools participate in the existing consumer tool handling path. Relevant file: ```text src/api/entities_api/orchestration/mixins/consumer_tool_handlers_mixin.py ``` Routing checks whether a model-emitted tool name has a bound MCP executor. ```text model tool call | v provider_name | v +----------------------------+ | MCP executor bound? | +-------------+--------------+ | +------+------+ | | yes no | | v v MCP executor existing execution consumer/tool path ``` This is important because MCP is additive. Unbound tool names continue through the pre-existing consumer handoff behavior. --- # 17. Reserved internal names MCP provider aliases may not shadow Project David internal routed tools. The consumer tool handler maintains a reserved-name set containing platform capabilities such as internal execution, web, delegation, and scratchpad tools. When an MCP executor is bound: ```text provider_name collides with internal routed name | v reject ``` This protects routing determinism. The discovery layer also attempts to avoid provider-facing collisions earlier in the lifecycle. --- # 18. Rich MCP results MCP results can contain more than text. Current Core preserves rich result data in `ToolResultEnvelope` while also maintaining the legacy string projection required by existing durable tool-output paths. Relevant files: ```text src/api/entities_api/orchestration/mcp_tool_executor.py src/api/entities_api/orchestration/tool_abi.py ``` The result model can preserve: ```text content structured_content content_blocks metadata is_error ``` MCP-specific metadata can include fields derived from the remote result, such as: ```text mcp_meta mcp_result_type ``` Conceptually: ```text MCP CallToolResult | v +-----------------------------+ | McpToolExecutor._adapt_result| +--------------+--------------+ | v +-----------------------------+ | ToolResultEnvelope | | | | content | | structured_content | | content_blocks | | metadata | | is_error | +--------------+--------------+ | +---- rich representation | +---- legacy string projection ``` This keeps MCP fidelity without forcing the entire older persistence pipeline to change at once. --- # 19. Long-running synchronous MCP calls Project David remains lifecycle authority while an MCP `tools/call` is running. Long-running execution support spans: ```text src/api/entities_api/orchestration/mcp_remote_client.py src/api/entities_api/orchestration/mcp_tool_executor.py src/api/entities_api/orchestration/mixins/consumer_tool_handlers_mixin.py ``` The remote client can forward MCP progress notifications. The executor forwards the progress callback. The consumer tool handler runs the remote call in its own asyncio task and continues checking the authoritative Project David Run state. ```text Project David Run | | authoritative status v model call --> asyncio MCP task --> remote tools/call | +---- progress callback | +---- periodic Run cancellation check ``` If the Project David Run becomes cancelled: ```text cancel remote execution task | v mark Action cancelled | v do not persist synthetic tool output ``` This preserves the rule that the remote MCP server does not own the Project David Run lifecycle. --- # 20. MCP progress is observational MCP progress notifications are passed through the transport and executor boundary. Core currently treats progress as execution telemetry rather than as a separate durable MCP task resource. This distinction matters: ```text long-running tools/call != durable MCP Tasks protocol resource ``` The managed architecture documented here is based on normal MCP discovery and synchronous `tools/call`, with Project David Run cancellation layered around it. --- # 21. Management API surface The MCP router is: ```text src/api/entities_api/routers/mcp_router.py ``` It exposes the managed registration and assistant attachment surfaces. Conceptual route groups: ```text MCP registrations POST /mcp/servers GET /mcp/servers GET /mcp/servers/{server_id} PATCH /mcp/servers/{server_id} DELETE /mcp/servers/{server_id} Discovery GET /mcp/servers/{server_id}/tools Assistant attachments GET /assistants/{assistant_id}/mcp-tools POST /assistants/{assistant_id}/mcp-tools DELETE /assistants/{assistant_id}/mcp-tools ``` The router remains thin. It authenticates the request, accepts validated contracts, and delegates policy and persistence to `McpRegistrationService`. --- # 22. State model The architecture has three categories of state. ## Registration state ```text McpServerRegistration ``` Durable description of a user-owned remote server endpoint. ## Attachment state ```text AssistantMcpTool Assistant.tool_configs ``` Durable description of which remote tools an assistant may expose, plus the model-facing definitions. ## Runtime state ```text McpToolExecutor _mcp_tool_executors registry active RemoteMcpClient session async execution task progress callback ``` Ephemeral state used while an orchestrator instance is running. ```text +----------------------+ +-----------------------+ | Durable DB state | | Ephemeral runtime | | | | | | McpServerRegistration|------->| RemoteMcpClient | | AssistantMcpTool | | McpToolExecutor | | Assistant.tool_configs|------>| executor registry | +----------------------+ | asyncio task | +-----------------------+ ``` --- # 23. Identity model The managed design uses multiple identity layers on purpose. ```text registration.id | | stable remote server registration v server_id remote MCP server | | protocol-advertised name v remote_name Project David | | stable logical identity v canonical_id model-facing namespace | | collision-safe alias v provider_name ``` These should not be collapsed into one string. They answer different questions: | Field | Meaning | |---|---| | `server_id` | Which managed server registration owns this tool? | | `remote_name` | What name must be sent to the remote MCP server? | | `canonical_id` | What is Project David's stable logical identity for the tool? | | `provider_name` | What function name does the model and router see? | --- # 24. Capability model The architecture distinguishes availability from authorization. ```text remote server advertises tool | v discoverable | | explicit attachment v assistant capability | | runtime binding v executable ``` A tool being discoverable does not automatically make it part of any assistant. A tool being attached durably does not mean the remote endpoint is always reachable. A tool being model-visible still executes under Project David's Run, Action, and error handling rules. --- # 25. Source ownership map ## MCP-specific Core files ### `src/api/entities_api/routers/mcp_router.py` Owns: - authenticated MCP management API - registration endpoints - discovery endpoint - assistant MCP attachment endpoints Depends on: - `McpRegistrationService` - auth and DB dependencies - shared validation contracts --- ### `src/api/entities_api/services/mcp_registration_service.py` Owns: - registration identity - URL normalization - ownership checks - registration CRUD - discovery orchestration - discovery pagination for attachment - attachment validation - durable `AssistantMcpTool` persistence - `Assistant.tool_configs` synchronization - detachment - registration deletion cleanup - assistant cache invalidation This is the center of the managed MCP control plane. --- ### `src/api/entities_api/services/mcp_tool_config.py` Owns helper logic for: - reading function tool names - upserting model-facing function tools - removing function tools Used to keep assistant tool configuration coherent with MCP provenance. --- ### `src/api/entities_api/orchestration/mcp_remote_client.py` Owns: - official MCP SDK client integration - Streamable HTTP session lifecycle - endpoint validation - server handshake metadata - `tools/list` - `tools/call` - progress callback forwarding This is the protocol transport boundary. --- ### `src/api/entities_api/orchestration/mcp_tool_discovery.py` Owns: - MCP Tool to Project David tool adaptation - `McpDiscoveredTool` - discovery page representation - canonical IDs - provider-safe aliases - collision handling - Project David `ToolDefinition` creation This is the identity and schema adaptation boundary. --- ### `src/api/entities_api/orchestration/mcp_tool_executor.py` Owns: - execution of one discovered MCP tool - provider-name validation - mapping provider alias back to remote name - remote `tools/call` - MCP error adaptation - rich `CallToolResult` preservation - legacy result projection This is the execution adapter boundary. --- ### `src/api/entities_api/orchestration/tool_abi.py` Owns the provider-neutral internal execution contracts: - `ToolCallEnvelope` - `ToolResultEnvelope` - `ToolExecutor` - tool definition primitives MCP uses this ABI rather than introducing a parallel orchestration lifecycle. --- ### `src/api/entities_api/orchestration/mixins/consumer_tool_handlers_mixin.py` Owns runtime MCP routing integration: - ephemeral executor registry - bind/unbind operations - reserved internal tool protection - lookup by `provider_name` - MCP execution handoff - Run/Action integration - long-running call cancellation - progress callback handling - fallback to existing consumer tool behavior This is the main MCP-to-orchestrator integration point. --- ## Supporting Core files ### `src/api/entities_api/services/assistants_service.py` MCP-aware assistant service integration. Uses enabled MCP attachment provider names when reasoning about assistant tool configuration. --- ### `src/api/entities_api/utilities/cache_utils.py` Supplies assistant cache invalidation used after MCP capability mutations. --- ### `src/api/entities_api/dependencies.py` Supplies request-scoped API authentication and database dependencies used by the MCP router. --- ### `src/api/entities_api/models/models.py` Contains the Core `Assistant` model used by the MCP registration service, including the assistant's `tool_configs` surface. --- ### `src/api/entities_api/routers/__init__.py` Participates in router aggregation so the MCP management router becomes part of the API surface. --- # 26. External package dependencies The managed design also depends on contracts outside Core. ## `projectdavid_common` Provides validation contracts exposed through `ValidationInterface`, including MCP registration and assistant attachment request/response models. Examples include: ```text McpServerRegistrationCreate McpServerRegistrationRead McpServerRegistrationUpdate AssistantMcpToolsAttach AssistantMcpToolsDetach AssistantMcpToolRead ``` ## `projectdavid_orm` Provides durable persistence models: ```text McpServerRegistration AssistantMcpTool ``` Core owns the service policy around these models, while ORM owns their persistence definitions. ## Official MCP Python SDK Provides protocol-native types and transport behavior used by `RemoteMcpClient`, discovery adaptation, and execution. --- # 27. Architectural invariants The current managed design depends on a small set of important invariants. ### Project David owns capability policy Remote discovery never automatically grants assistant access. ### Project David owns lifecycle Remote execution does not replace Project David Run and Action state. ### Registration identity is stable Mutable registration names are not used as durable server identity. ### Remote names remain remote names `remote_name` is preserved for protocol execution. ### Provider names are Project David names `provider_name` belongs to the model-facing routing namespace. ### Attached aliases are durable Metadata refresh does not casually rename an already attached capability. ### Tool configuration and provenance move together `AssistantMcpTool` and `Assistant.tool_configs` must remain coherent. ### MCP transport stays narrow `RemoteMcpClient` does not become the policy, persistence, or orchestration layer. ### Runtime bindings are ephemeral The executor registry belongs to an orchestrator instance, not to durable persistence. ### Internal tool names cannot be shadowed MCP aliases must not collide with reserved platform tools. ### Remote failures remain tool failures Transport and server errors are adapted into the normal tool execution lifecycle. ### Cancellation authority stays local Cancelling a Project David Run can interrupt a long-running MCP call. --- # 28. Conceptual end-to-end flow ```text 1. REGISTER User | v POST /mcp/servers | v McpRegistrationService | v McpServerRegistration 2. DISCOVER McpServerRegistration | v RemoteMcpClient | | tools/list v Remote MCP server | v mcp_tool_discovery | v McpDiscoveredTool[] 3. ATTACH selected remote_name[] | v McpRegistrationService | +---- validate against current discovery | +---- create/update AssistantMcpTool | +---- upsert definition into Assistant.tool_configs | +---- commit | +---- invalidate assistant cache 4. RUNTIME Assistant capability | v provider-facing definition | v model emits provider_name | v ConsumerToolHandlersMixin | v McpToolExecutor | v RemoteMcpClient | | tools/call(remote_name, arguments) v Remote MCP server 5. RESULT CallToolResult | v McpToolExecutor | v ToolResultEnvelope | +---- rich MCP data | +---- legacy string projection | v Project David tool output / Action / Run lifecycle ``` --- # 29. Design summary Project David's MCP implementation is not a thin proxy. It is a managed capability system built around MCP as a remote protocol. ```text MCP provides: server handshake tool discovery tool schemas tool execution protocol result types progress events Project David provides: ownership registration stable identity provider naming explicit assistant attachment tool configuration routing Run lifecycle Action lifecycle cancellation persistence cache coherence compatibility with existing tools ``` That division is the core of the architecture. It lets MCP capabilities participate in Project David without making the rest of the system MCP-specific.