TechByteByByte

Capstone: Building an Enterprise MCP-Powered AI Assistant

Every real piece from this course, assembled file by file — real servers, real security, real LangGraph orchestration, and genuine human-in-the-loop approval, built the way a real, production deployment actually would be.

#MCP#Capstone#Enterprise#LangGraph

Every module in this course has been building toward this one. Recall Module 18’s own closing promise — this is the complete, real system: an enterprise assistant reaching GitHub, a CRM, and internal documentation, all through real MCP servers, orchestrated by real LangGraph, with genuine, deliberate security throughout.

The real, complete architecture

flowchart TD
    U[User] --> A[AI Assistant]
    A --> G[LangGraph]
    G --> M[MCP Clients]
    M --> S1[GitHub MCP]
    M --> S2[CRM MCP]
    M --> S3[Docs MCP]
    S1 --> API1[GitHub API]
    S2 --> API2[CRM Database]
    S3 --> API3[Internal Knowledge]

The real, complete project structure

enterprise-mcp/
├── host/
│   ├── main.py
│   ├── graph.py
│   ├── mcp_clients.py
│   └── config.py
├── servers/
│   ├── github_server.py
│   ├── crm_server.py
│   └── docs_server.py
├── security/
│   ├── auth.py
│   └── policy.py
├── tests/
├── .env.example
└── requirements.txt

servers/docs_server.py — a real, complete server

Recall Module 9’s own progressive server build — this is that same, real discipline, applied to genuine documentation search.

# servers/docs_server.py
import logging
from fastmcp import FastMCP

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("docs-server")

mcp = FastMCP("docs-server")

DOCS = {"onboarding.md": "New hires complete onboarding within their first week.", "security.md": "All API keys rotate every 90 days."}

@mcp.tool()
def search_docs(query: str) -> list[dict]:
    """Search internal documentation for content matching the query."""
    logger.info(f"Searching docs for: {query}")  # recall Module 15's own real logging discipline
    return [{"file": name, "snippet": text} for name, text in DOCS.items() if query.lower() in text.lower()]

@mcp.resource("policy://security")
def security_policy() -> str:
    """The company's real, current security policy."""
    return DOCS["security.md"]

if __name__ == "__main__":
    mcp.run()

crm_server.py and github_server.py follow this exact, real, same shape — recall Module 5’s own progressive tool examples — each exposing tools and resources scoped to its own, distinct, real domain.

security/policy.py — real, deliberate tool permission filtering

Recall Module 14’s own real, structural safeguard directly — this is that same function, made into a genuine, reusable policy module.

# security/policy.py
READ_TOOLS = {"search_docs", "get_customer", "search_repos"}
WRITE_TOOLS = {"create_ticket", "update_crm_record"}  # a real, deliberately smaller, scrutinized set

def filter_tools_by_permission(tools: list, user_role: str) -> list:
    """A real, deliberate permission layer — recall Module 14's own structural safeguard."""
    allowed = READ_TOOLS | (WRITE_TOOLS if user_role == "admin" else set())
    return [t for t in tools if t.name in allowed]

security/auth.py — real, deliberate credential handling

Recall Module 14’s own real OAuth 2.1 discussion — this module keeps real credentials out of application code entirely.

# security/auth.py
import os

def get_mcp_credentials() -> dict:
    """Load real credentials from the environment — never hardcoded, recall Module 5's own code-quality discipline."""
    return {
        "github_token": os.environ["GITHUB_MCP_TOKEN"],
        "crm_api_key": os.environ["CRM_MCP_API_KEY"],
    }

host/mcp_clients.py — real, multi-server connection management

Recall Module 12’s own real MultiServerMCPClient — this is that same, real pattern, wired to this capstone’s own three, genuine servers.

# host/mcp_clients.py
from langchain_mcp_adapters.client import MultiServerMCPClient
from security.policy import filter_tools_by_permission

async def get_tools_for_user(user_role: str) -> list:
    client = MultiServerMCPClient({
        "github": {"command": "python", "args": ["servers/github_server.py"], "transport": "stdio"},
        "crm": {"command": "python", "args": ["servers/crm_server.py"], "transport": "stdio"},
        "docs": {"command": "python", "args": ["servers/docs_server.py"], "transport": "stdio"},
    })
    all_tools = await client.get_tools()  # recall Module 12's own real, multi-server discovery
    return filter_tools_by_permission(all_tools, user_role)  # recall Module 14's own real, deliberate filter

host/graph.py — real LangGraph orchestration with human approval

Recall Module 14’s own real interrupt() discussion — this is where that safeguard becomes genuine, working code.

# host/graph.py
from typing import TypedDict
from langgraph.graph import StateGraph, START, END
from langgraph.types import interrupt
from langchain.chat_models import init_chat_model
from host.mcp_clients import get_tools_for_user

class State(TypedDict):
    question: str
    user_role: str
    answer: str

async def agent_node(state: State) -> dict:
    tools = await get_tools_for_user(state["user_role"])  # real, permission-filtered tools
    model = init_chat_model("openai:gpt-4o-mini").bind_tools(tools)
    response = await model.ainvoke(state["question"])

    # recall Module 14's own real human-in-the-loop discipline for write-capable actions
    if response.tool_calls and any(tc["name"] in {"create_ticket", "update_crm_record"} for tc in response.tool_calls):
        approved = interrupt({"action": "approve_write", "calls": response.tool_calls})
        if not approved:
            return {"answer": "Action cancelled — write actions require real, explicit approval."}

    return {"answer": response.content}

builder = StateGraph(State)
builder.add_node("agent", agent_node)
builder.add_edge(START, "agent")
builder.add_edge("agent", END)
graph = builder.compile()

host/main.py — tying every real piece together

# host/main.py
import asyncio
from host.graph import graph

async def main():
    result = await graph.ainvoke({"question": "What's our security policy?", "user_role": "employee", "answer": ""})
    print(result["answer"])

if __name__ == "__main__":
    asyncio.run(main())

Walking through the real, complete flow

A real, incoming question enters main.py, which invokes the real, compiled graph. Inside graph.py’s own agent_node, get_tools_for_user connects to all three, real servers, discovers their genuine tools, and filters them down to exactly what this specific, real user’s role permits — recall policy.py’s own real, deliberate distinction between read and write tools. If the model genuinely decides a write action is needed, interrupt() pauses the entire, real graph, exactly the same safeguard Module 14 argued was non-negotiable for consequential, real actions.

Common mistakes worth avoiding

Skipping the permission filter and handing every discovered tool to every real user. Recall policy.py’s own real, deliberate distinction — this single function is what stands between a genuine, honest assistant and the exact, structural risk Module 14’s own real incidents demonstrated.

Hardcoding real credentials directly in mcp_clients.py. Recall auth.py’s own real, deliberate separation — credentials belong in environment variables, loaded once, never scattered through application code.

Forgetting the human-approval check for write-capable tools. Recall graph.py’s own real interrupt() call — without it, this capstone would repeat the exact, structural mistake behind Module 14’s own real, documented incidents.

What you should take away from this module

  • A real, complete MCP application is genuinely the sum of everything this course covered — real servers, real security, real orchestration — not one, single, complex abstraction.
  • Permission filtering and human-in-the-loop approval for write actions are the real, structural safeguards standing between a genuinely useful assistant and a genuine, documented incident.
  • LangGraph’s real interrupt(), combined with MCP’s own real tool discovery, gives you a complete, honest, production-shaped pattern for exactly the kind of consequential, real actions this course warned about from Module 5 onward.

Closing this course

You began Module 1 with a genuinely honest problem: every AI application reinventing the same, real integrations. Nineteen modules later, you have the real, complete answer — not just how MCP’s protocol works, but how to build it, secure it, debug it, and deploy it the way real, documented companies like Pinterest actually have, while staying honest about the real, current gaps — only 8.5% of deployed servers implementing mandatory OAuth 2.1 — that this course never smoothed over. That honesty, as much as the code itself, is what a genuinely production-ready understanding of MCP actually looks like.

Author
TechByteByByte Editorial Team
Reviewed by
TechByteByByte Admin
Published
Last reviewed