Skip to content

Latest commit

 

History

History
84 lines (45 loc) · 5.78 KB

File metadata and controls

84 lines (45 loc) · 5.78 KB

Architecture Notes

Design decisions and reasoning behind the MCP composition pattern, for anyone who wants to go deeper than the README.


Why mount pattern instead of four separate ports

The natural instinct when building four specialized MCP servers is to run them on four different ports: 8001, 8002, 8003, 8004. That is the right production architecture for independent scaling and deployment.

For this demo, the mount pattern is the better choice. FastMCP lets you mount sub-servers onto a parent server, each at its own path prefix. All four servers run on port 8000. One Bearer token, one ngrok tunnel, one startup command.

The composition story is identical either way. The LangGraph orchestrator treats each mounted server as a distinct service because each has its own tool namespace. /config/get_deployment_config is a different tool from /agent/download_build. The mount pattern demonstrates composition clearly without the setup overhead of four separate processes.

Production note: if you need to scale or deploy servers independently, split them. The shared infrastructure (logger, session_manager, cache, auth) is already packaged as a separate module for exactly this reason.


Why LangGraph instead of LangChain ReAct for the orchestrator

The deployment pipeline has a fixed sequence: fetch config, then deploy, then audit, then notify. You cannot notify before auditing. You cannot deploy before fetching config.

LangChain ReAct lets the LLM decide tool order at runtime. For an open-ended task where the execution path cannot be predetermined, that flexibility is exactly right. For a deployment pipeline where sequence matters and skipping a step has consequences, it is the wrong tool.

LangGraph enforces the sequence through nodes and edges. The edge from fetch_config to execute_deployment is a guarantee, not a suggestion. The LLM cannot decide to skip the audit step because there is no edge that bypasses it.

The practical difference: in the LangChain version, a confusingly-worded user request might cause the agent to call deploy_on_server before transfer_to_server completes. In the LangGraph version, that is structurally impossible.


How the servers communicate without talking to each other

MCP servers do not call other MCP servers. That would create tight coupling and make the architecture fragile. If Server 3 depended on Server 1 being available, a restart of Server 1 would break Server 3.

Instead, the LangGraph State object carries data between nodes. Each node calls one server's tools, receives results, and stores them in State. The next node reads from State and passes relevant data as tool arguments.

Node 1 calls Config Server --> stores config in State
Node 2 reads config from State --> passes server_ip as argument to agent tools
Node 3 reads deployment result from State --> creates audit record
Node 4 reads audit_id from State --> includes it in notification

Redis session memory plays a supporting role. Each server uses Redis internally for caching and short-term session context. The servers write to Redis as a side effect of tool calls. But the primary coordination mechanism is LangGraph State, not Redis. The orchestrator never reads from Redis directly.


The session_id as the coordination key

Every tool call receives an optional session_id. This serves two purposes.

First, it enables structured logging. Every log line for a given deployment run shares the same session_id, making it trivial to filter logs for a specific deployment.

Second, it scopes Redis session memory correctly. The key pattern session:{session_id}:{server}:{data_type} ensures User A and User B never share session data. A session isolation bug in production is a data breach. The session_id prefix prevents it.

The session_id is generated once at the start of each run_deployment() call and passed through every node via State.


Why the cache is transparent to the orchestrator

The Config Server caches deployment configs for 60 seconds internally. The orchestrator does not know or care whether a response came from cache or from the config store. It just calls get_deployment_config and gets a result.

This is the right design. Caching is an infrastructure concern. The orchestrator's job is to coordinate the deployment workflow. If the caching logic changes, the orchestrator does not change.

The same principle applies to retry logic inside execute_deployment_node. The node retries transfer_to_server up to three times if checksum verification fails. This retry is explicit in the orchestrator code, not delegated to the LLM. LangGraph makes the retry logic visible and testable.


Why the audit node runs whether deployment succeeds or fails

Every deployment attempt must be recorded, not just successful ones. Failed deployments are often the most important records for debugging and post-mortems.

The edge from execute_deployment to audit is unconditional. There is no conditional edge that skips auditing on failure. The audit node reads deployment_success from State and records the correct final status accordingly.

This is a deliberate design decision. If you add a conditional edge that bypasses audit on failure, you will eventually lose a failure record that you needed. The audit node is cheap to run and the record is always valuable.


ngrok and the free plan limitation

The mount pattern solves the main ngrok free plan constraint. The free plan allows one concurrent tunnel. With four separate servers on four ports, you would need four tunnels and a paid plan. With the mount pattern, one tunnel on port 8000 exposes all four servers simultaneously.

If you move to separate ports in production, use ngrok's configuration file to define multiple tunnels in one session, or use a static subdomain on the paid plan to avoid URL changes on restart.