<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:taxo="http://purl.org/rss/1.0/modules/taxonomy/" version="2.0">
  <channel>
    <title>topic Building a Custom FastMCP Server on Databricks - What the Managed Genie Can't Do. in Community Articles</title>
    <link>https://community.databricks.com/t5/community-articles/building-a-custom-fastmcp-server-on-databricks-what-the-managed/m-p/162220#M1348</link>
    <description>&lt;P class=""&gt;&lt;STRONG&gt;I Built a Custom MCP Server on Databricks — Here’s What the Managed Version Can’t Do&lt;/STRONG&gt;&lt;/P&gt;&lt;P class=""&gt;&lt;EM&gt;Part 2 of the MCP on Databricks series&lt;/EM&gt;&lt;/P&gt;&lt;P class=""&gt;In Part 1, I covered what Genie MCP and DBSQL MCP give you out of the box — and where they stop.&lt;/P&gt;&lt;P class=""&gt;The short version: both are stateless, the managed Genie MCP has no `conversation_id` control, and the moment you need multi-turn context or cross-session memory, you’ve hit a wall.&lt;/P&gt;&lt;P class=""&gt;This post is about what I built to get past that wall.&lt;/P&gt;&lt;P class=""&gt;&lt;STRONG&gt;The Problems I Set Out to Solve&lt;/STRONG&gt;&lt;/P&gt;&lt;P class=""&gt;After testing the managed Genie MCP in a real agentic workflow, I ran into four specific gaps that couldn’t be patched at the prompt level:&lt;/P&gt;&lt;P class=""&gt;&lt;STRONG&gt;1. No conversation_id control in the per-space Genie MCP&lt;/STRONG&gt;&lt;/P&gt;&lt;P class=""&gt;Every query_space tool call hits a single cross-space endpoint (/api/2.0/mcp/genie/{space_id}) and starts a brand new Genie conversation under the hood — stateless by design. You lose per-space routing, fine-grained tool descriptions, and any sense of session continuity.&lt;/P&gt;&lt;P class=""&gt;&lt;STRONG&gt;2. No cross-session memory&lt;/STRONG&gt;&lt;/P&gt;&lt;P class=""&gt;Even if LangGraph maintains state within a session via its checkpointer, once the session ends, the Genie-side context is gone. There’s nothing to resume from.&lt;/P&gt;&lt;P class=""&gt;&lt;STRONG&gt;3. No custom routing logic&lt;/STRONG&gt;&lt;/P&gt;&lt;P class=""&gt;The managed MCP exposes all Genie spaces as tools but gives you zero control over how the agent picks between them. The tool descriptions are too generic to drive reliable routing.&lt;/P&gt;&lt;P class=""&gt;&lt;STRONG&gt;4. STM lives outside MCP&lt;/STRONG&gt;&lt;/P&gt;&lt;P class=""&gt;Short-term memory is managed at the LangGraph layer, not at the MCP layer. This means the MCP server itself is stateless — each tool call is atomic and knows nothing about previous calls in the same session.&lt;/P&gt;&lt;P class=""&gt;The managed Genie MCP can’t solve any of these. So I built a custom FastMCP server on top of the Genie REST API.&lt;/P&gt;&lt;P class=""&gt;&lt;STRONG&gt;Architecture&lt;/STRONG&gt;&lt;/P&gt;&lt;P class=""&gt;Before diving into the code, here’s how the pieces fit together:&lt;/P&gt;&lt;DIV class=""&gt;&lt;SPAN class=""&gt;Press enter or click to view image in full size&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;span class="lia-inline-image-display-wrapper lia-image-align-inline" image-alt="sagarpgowda7777_0-1783508005850.jpeg" style="width: 400px;"&gt;&lt;img src="https://community.databricks.com/t5/image/serverpage/image-id/28734iC888F3A40F966F05/image-size/medium?v=v2&amp;amp;px=400" role="button" title="sagarpgowda7777_0-1783508005850.jpeg" alt="sagarpgowda7777_0-1783508005850.jpeg" /&gt;&lt;/span&gt;&lt;P&gt;&amp;nbsp;&lt;/P&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;P class=""&gt;The key insight: `conversation_id` is the bridge between LangGraph state and Genie’s conversation thread. Persist it, and you get continuity. Drop it, and you’re back to square one every call.&lt;/P&gt;&lt;P class=""&gt;&lt;STRONG&gt;Building the Custom FastMCP Server&lt;/STRONG&gt;&lt;/P&gt;&lt;P class=""&gt;&lt;STRONG&gt;Step 1 — Basic Server Skeleton&lt;/STRONG&gt;&lt;/P&gt;&lt;PRE&gt;&lt;SPAN class=""&gt;&lt;SPAN class=""&gt;from&lt;/SPAN&gt; fastmcp &lt;SPAN class=""&gt;import&lt;/SPAN&gt; FastMCP&lt;BR /&gt;&lt;SPAN class=""&gt;import&lt;/SPAN&gt; requests&lt;BR /&gt;&lt;SPAN class=""&gt;import&lt;/SPAN&gt; os&lt;BR /&gt;&lt;SPAN class=""&gt;import&lt;/SPAN&gt; time&lt;BR /&gt;mcp = FastMCP(&lt;SPAN class=""&gt;"databricks-genie-mcp"&lt;/SPAN&gt;)&lt;BR /&gt;DATABRICKS_HOST = os.environ[&lt;SPAN class=""&gt;"DATABRICKS_HOST"&lt;/SPAN&gt;]&lt;BR /&gt;DATABRICKS_TOKEN = os.environ[&lt;SPAN class=""&gt;"DATABRICKS_TOKEN"&lt;/SPAN&gt;]&lt;BR /&gt;GENIE_SPACE_ID = os.environ[&lt;SPAN class=""&gt;"GENIE_SPACE_ID"&lt;/SPAN&gt;]&lt;BR /&gt;HEADERS = {&lt;BR /&gt;&lt;SPAN class=""&gt;"Authorization"&lt;/SPAN&gt;: &lt;SPAN class=""&gt;f"Bearer &lt;SPAN class=""&gt;{DATABRICKS_TOKEN}&lt;/SPAN&gt;"&lt;/SPAN&gt;,&lt;BR /&gt;&lt;SPAN class=""&gt;"Content-Type"&lt;/SPAN&gt;: &lt;SPAN class=""&gt;"application/json"&lt;/SPAN&gt;&lt;BR /&gt;}&lt;BR /&gt;&lt;SPAN class=""&gt;def&lt;/SPAN&gt; &lt;SPAN class=""&gt;genie_api&lt;/SPAN&gt;(&lt;SPAN class=""&gt;endpoint: &lt;SPAN class=""&gt;str&lt;/SPAN&gt;, method: &lt;SPAN class=""&gt;str&lt;/SPAN&gt; = &lt;SPAN class=""&gt;"GET"&lt;/SPAN&gt;, payload: &lt;SPAN class=""&gt;dict&lt;/SPAN&gt; = &lt;SPAN class=""&gt;None):&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;BR /&gt;    url = &lt;SPAN class=""&gt;f"&lt;SPAN class=""&gt;{DATABRICKS_HOST}&lt;/SPAN&gt;/api/2.0/genie/spaces/&lt;SPAN class=""&gt;{GENIE_SPACE_ID}&lt;/SPAN&gt;/&lt;SPAN class=""&gt;{endpoint}&lt;/SPAN&gt;"&lt;/SPAN&gt;&lt;BR /&gt;    response = requests.request(method, url, headers=HEADERS, json=payload)&lt;BR /&gt;    response.raise_for_status()&lt;BR /&gt;&lt;SPAN class=""&gt;    return&lt;/SPAN&gt; response.json()&lt;/SPAN&gt;&lt;/PRE&gt;&lt;P class=""&gt;Nothing surprising here — FastMCP server wrapping the Genie REST API with a shared helper for authenticated requests.&lt;/P&gt;&lt;P class=""&gt;&lt;STRONG&gt;Step 2 — The Core: conversation_id Persistence&lt;/STRONG&gt;&lt;/P&gt;&lt;P class=""&gt;This is the piece the managed Genie MCP simply doesn’t give you.&lt;/P&gt;&lt;PRE&gt;&lt;SPAN class=""&gt;&lt;SPAN class=""&gt;@mcp.tool()&lt;/SPAN&gt;&lt;BR /&gt;&lt;SPAN class=""&gt;def&lt;/SPAN&gt; &lt;SPAN class=""&gt;create_new_conversation&lt;/SPAN&gt;() -&amp;gt; &lt;SPAN class=""&gt;dict&lt;/SPAN&gt;:&lt;BR /&gt;&lt;SPAN class=""&gt;"""&lt;BR /&gt;Start a new Genie conversation and return the conversation_id.&lt;BR /&gt;Store this in LangGraph state to enable multi-turn context.&lt;BR /&gt;"""&lt;/SPAN&gt;&lt;BR /&gt;result = genie_api(&lt;BR /&gt;&lt;SPAN class=""&gt;"start-conversation"&lt;/SPAN&gt;,&lt;BR /&gt;method=&lt;SPAN class=""&gt;"POST"&lt;/SPAN&gt;,&lt;BR /&gt;payload={&lt;SPAN class=""&gt;"content"&lt;/SPAN&gt;: &lt;SPAN class=""&gt;"Session initialized."&lt;/SPAN&gt;}&lt;BR /&gt;)&lt;BR /&gt;&lt;SPAN class=""&gt;return&lt;/SPAN&gt; {&lt;BR /&gt;&lt;SPAN class=""&gt;"conversation_id"&lt;/SPAN&gt;: result[&lt;SPAN class=""&gt;"conversation"&lt;/SPAN&gt;][&lt;SPAN class=""&gt;"conversation_id"&lt;/SPAN&gt;],&lt;BR /&gt;&lt;SPAN class=""&gt;"initial_message_id"&lt;/SPAN&gt;: result[&lt;SPAN class=""&gt;"message"&lt;/SPAN&gt;][&lt;SPAN class=""&gt;"message_id"&lt;/SPAN&gt;],&lt;BR /&gt;&lt;SPAN class=""&gt;"message"&lt;/SPAN&gt;: &lt;SPAN class=""&gt;"New conversation created. Pass this conversation_id&lt;BR /&gt;to all subsequent query_genie calls."&lt;/SPAN&gt;&lt;BR /&gt;}&lt;BR /&gt;&lt;SPAN class=""&gt;@mcp.tool()&lt;/SPAN&gt;&lt;BR /&gt;&lt;SPAN class=""&gt;def&lt;/SPAN&gt; &lt;SPAN class=""&gt;query_genie_with_context&lt;/SPAN&gt;(&lt;SPAN class=""&gt;question: &lt;SPAN class=""&gt;str&lt;/SPAN&gt;, conversation_id: &lt;SPAN class=""&gt;str&lt;/SPAN&gt;&lt;/SPAN&gt;) -&amp;gt; &lt;SPAN class=""&gt;dict&lt;/SPAN&gt;:&lt;BR /&gt;&lt;SPAN class=""&gt;"""&lt;BR /&gt;Query Genie within an existing conversation thread.&lt;BR /&gt;Requires conversation_id from a previous create_new_conversation call.&lt;BR /&gt;"""&lt;/SPAN&gt;&lt;BR /&gt;&lt;SPAN class=""&gt;# Send message to existing conversation&lt;/SPAN&gt;&lt;BR /&gt;message_result = genie_api(&lt;BR /&gt;&lt;SPAN class=""&gt;f"conversations/&lt;SPAN class=""&gt;{conversation_id}&lt;/SPAN&gt;/messages"&lt;/SPAN&gt;,&lt;BR /&gt;method=&lt;SPAN class=""&gt;"POST"&lt;/SPAN&gt;,&lt;BR /&gt;payload={&lt;SPAN class=""&gt;"content"&lt;/SPAN&gt;: question}&lt;BR /&gt;)&lt;BR /&gt;message_id = message_result[&lt;SPAN class=""&gt;"message_id"&lt;/SPAN&gt;]&lt;/SPAN&gt;&lt;/PRE&gt;&lt;P class=""&gt;Poll for result (Genie is async)&lt;/P&gt;&lt;PRE&gt;&lt;SPAN class=""&gt;&lt;SPAN class=""&gt;for&lt;/SPAN&gt; _ &lt;SPAN class=""&gt;in&lt;/SPAN&gt; &lt;SPAN class=""&gt;range&lt;/SPAN&gt;(&lt;SPAN class=""&gt;30):&lt;/SPAN&gt;&lt;BR /&gt;status = genie_api(&lt;BR /&gt;&lt;SPAN class=""&gt;f"conversations/&lt;SPAN class=""&gt;{conversation_id}&lt;/SPAN&gt;/messages/&lt;SPAN class=""&gt;{message_id}&lt;/SPAN&gt;"&lt;/SPAN&gt;&lt;BR /&gt;)&lt;BR /&gt;&lt;SPAN class=""&gt;if&lt;/SPAN&gt; status[&lt;SPAN class=""&gt;"status"&lt;/SPAN&gt;] == &lt;SPAN class=""&gt;"COMPLETED"&lt;/SPAN&gt;:&lt;BR /&gt;&lt;SPAN class=""&gt;return&lt;/SPAN&gt; {&lt;BR /&gt;&lt;SPAN class=""&gt;"answer"&lt;/SPAN&gt;: status.get(&lt;SPAN class=""&gt;"content"&lt;/SPAN&gt;, &lt;SPAN class=""&gt;""&lt;/SPAN&gt;),&lt;BR /&gt;&lt;SPAN class=""&gt;"conversation_id"&lt;/SPAN&gt;: conversation_id,&lt;BR /&gt;&lt;SPAN class=""&gt;"message_id"&lt;/SPAN&gt;: message_id&lt;BR /&gt;}&lt;BR /&gt;&lt;SPAN class=""&gt;elif&lt;/SPAN&gt; status[&lt;SPAN class=""&gt;"status"&lt;/SPAN&gt;] == &lt;SPAN class=""&gt;"FAILED"&lt;/SPAN&gt;:&lt;BR /&gt;&lt;SPAN class=""&gt;return&lt;/SPAN&gt; {&lt;SPAN class=""&gt;"error"&lt;/SPAN&gt;: &lt;SPAN class=""&gt;"Genie query failed"&lt;/SPAN&gt;, &lt;SPAN class=""&gt;"conversation_id"&lt;/SPAN&gt;: conversation_id}&lt;BR /&gt;time.sleep(&lt;SPAN class=""&gt;2&lt;/SPAN&gt;)&lt;BR /&gt;&lt;SPAN class=""&gt;return&lt;/SPAN&gt; {&lt;SPAN class=""&gt;"error"&lt;/SPAN&gt;: &lt;SPAN class=""&gt;"Timeout waiting for Genie response"&lt;/SPAN&gt;}&lt;/SPAN&gt;&lt;/PRE&gt;&lt;P class=""&gt;The critical difference from the managed MCP: `conversation_id` is passed explicitly on every call. Genie sees this as a continuation of the same conversation — it has full context of everything said before.&lt;/P&gt;&lt;P class=""&gt;&lt;STRONG&gt;Step 3 — Cross-Session Memory via History Retrieval&lt;/STRONG&gt;&lt;/P&gt;&lt;P class=""&gt;This is the tool the managed Genie MCP has no equivalent for.&lt;/P&gt;&lt;PRE&gt;&lt;SPAN class=""&gt;&lt;SPAN class=""&gt;@mcp.tool()&lt;/SPAN&gt;&lt;BR /&gt;&lt;SPAN class=""&gt;def&lt;/SPAN&gt; &lt;SPAN class=""&gt;get_conversation_history&lt;/SPAN&gt;(&lt;SPAN class=""&gt;conversation_id: &lt;SPAN class=""&gt;str&lt;/SPAN&gt;&lt;/SPAN&gt;) -&amp;gt; &lt;SPAN class=""&gt;dict&lt;/SPAN&gt;:&lt;BR /&gt;&lt;SPAN class=""&gt;"""&lt;BR /&gt;Retrieve full message history from a previous Genie conversation.&lt;BR /&gt;Use this at the start of a new session to hydrate context.&lt;BR /&gt;"""&lt;/SPAN&gt;&lt;BR /&gt;result = genie_api(&lt;SPAN class=""&gt;f"conversations/&lt;SPAN class=""&gt;{conversation_id}&lt;/SPAN&gt;/messages"&lt;/SPAN&gt;)&lt;BR /&gt;messages = []&lt;BR /&gt;&lt;SPAN class=""&gt;for&lt;/SPAN&gt; msg &lt;SPAN class=""&gt;in&lt;/SPAN&gt; result.get(&lt;SPAN class=""&gt;"messages"&lt;/SPAN&gt;, []):&lt;BR /&gt;messages.append({&lt;BR /&gt;&lt;SPAN class=""&gt;"role"&lt;/SPAN&gt;: msg.get(&lt;SPAN class=""&gt;"role"&lt;/SPAN&gt;),&lt;BR /&gt;&lt;SPAN class=""&gt;"content"&lt;/SPAN&gt;: msg.get(&lt;SPAN class=""&gt;"content"&lt;/SPAN&gt;),&lt;BR /&gt;&lt;SPAN class=""&gt;"timestamp"&lt;/SPAN&gt;: msg.get(&lt;SPAN class=""&gt;"created_at"&lt;/SPAN&gt;)&lt;BR /&gt;})&lt;BR /&gt;&lt;SPAN class=""&gt;return&lt;/SPAN&gt; {&lt;BR /&gt;&lt;SPAN class=""&gt;"conversation_id"&lt;/SPAN&gt;: conversation_id,&lt;BR /&gt;&lt;SPAN class=""&gt;"message_count"&lt;/SPAN&gt;: &lt;SPAN class=""&gt;len&lt;/SPAN&gt;(messages),&lt;BR /&gt;&lt;SPAN class=""&gt;"history"&lt;/SPAN&gt;: messages&lt;BR /&gt;}&lt;/SPAN&gt;&lt;/PRE&gt;&lt;P class=""&gt;At the start of a new session, the LangGraph agent calls `get_conversation_history()` with the persisted `conversation_id` from the previous session. It hydrates the context before asking anything new.&lt;/P&gt;&lt;P class=""&gt;This is what makes the system behave like it has memory — even though MCP itself is stateless.&lt;/P&gt;&lt;P class=""&gt;&lt;STRONG&gt;Wiring It Into LangGraph&lt;/STRONG&gt;&lt;/P&gt;&lt;P class=""&gt;&lt;STRONG&gt;STM vs LTM — Clarifying the Layers&lt;/STRONG&gt;&lt;/P&gt;&lt;P class=""&gt;LangGraph handles STM. The persisted `conversation_id` bridges LTM. The custom MCP server connects both to Genie.&lt;/P&gt;&lt;P class=""&gt;&lt;STRONG&gt;The LangGraph Graph&lt;/STRONG&gt;&lt;/P&gt;&lt;PRE&gt;&lt;SPAN class=""&gt;&lt;SPAN class=""&gt;from&lt;/SPAN&gt; langgraph.graph &lt;SPAN class=""&gt;import&lt;/SPAN&gt; StateGraph, END&lt;BR /&gt;&lt;SPAN class=""&gt;from&lt;/SPAN&gt; langgraph.checkpoint.memory &lt;SPAN class=""&gt;import&lt;/SPAN&gt; MemorySaver&lt;BR /&gt;&lt;SPAN class=""&gt;from&lt;/SPAN&gt; langchain_openai &lt;SPAN class=""&gt;import&lt;/SPAN&gt; ChatOpenAI&lt;BR /&gt;&lt;SPAN class=""&gt;from&lt;/SPAN&gt; typing &lt;SPAN class=""&gt;import&lt;/SPAN&gt; TypedDict, &lt;SPAN class=""&gt;Optional&lt;/SPAN&gt;&lt;BR /&gt;&lt;SPAN class=""&gt;class&lt;/SPAN&gt; &lt;SPAN class=""&gt;AgentState&lt;/SPAN&gt;(&lt;SPAN class=""&gt;TypedDict):&lt;/SPAN&gt;&lt;BR /&gt;messages: &lt;SPAN class=""&gt;list&lt;/SPAN&gt;&lt;BR /&gt;conversation_id: &lt;SPAN class=""&gt;Optional&lt;/SPAN&gt;[&lt;SPAN class=""&gt;str&lt;/SPAN&gt;]  &lt;SPAN class=""&gt;# Genie conversation_id persisted here&lt;/SPAN&gt;&lt;BR /&gt;question: &lt;SPAN class=""&gt;str&lt;/SPAN&gt;&lt;BR /&gt;answer: &lt;SPAN class=""&gt;Optional&lt;/SPAN&gt;[&lt;SPAN class=""&gt;str&lt;/SPAN&gt;]&lt;BR /&gt;llm = ChatOpenAI(&lt;BR /&gt;base_url=&lt;SPAN class=""&gt;f"&lt;SPAN class=""&gt;{host}&lt;/SPAN&gt;/serving-endpoints"&lt;/SPAN&gt;,&lt;BR /&gt;api_key=user_token,&lt;BR /&gt;model=LLM_ENDPOINT,&lt;BR /&gt;temperature=&lt;SPAN class=""&gt;0&lt;/SPAN&gt;,&lt;BR /&gt;)&lt;BR /&gt;&lt;SPAN class=""&gt;def&lt;/SPAN&gt; &lt;SPAN class=""&gt;initialize_session&lt;/SPAN&gt;(&lt;SPAN class=""&gt;state: AgentState&lt;/SPAN&gt;) -&amp;gt; AgentState:&lt;BR /&gt;&lt;SPAN class=""&gt;"""&lt;BR /&gt;At session start - either create new Genie conversation&lt;BR /&gt;or retrieve history from a previous one.&lt;BR /&gt;"""&lt;/SPAN&gt;&lt;BR /&gt;&lt;SPAN class=""&gt;if&lt;/SPAN&gt; state.get(&lt;SPAN class=""&gt;"conversation_id"):&lt;/SPAN&gt;&lt;BR /&gt;&lt;SPAN class=""&gt;# Resume existing conversation - hydrate context&lt;/SPAN&gt;&lt;BR /&gt;history = get_conversation_history(state[&lt;SPAN class=""&gt;"conversation_id"&lt;/SPAN&gt;])&lt;BR /&gt;state[&lt;SPAN class=""&gt;"messages"&lt;/SPAN&gt;].append({&lt;BR /&gt;&lt;SPAN class=""&gt;"role"&lt;/SPAN&gt;: &lt;SPAN class=""&gt;"system"&lt;/SPAN&gt;,&lt;BR /&gt;&lt;SPAN class=""&gt;"content"&lt;/SPAN&gt;: &lt;SPAN class=""&gt;f"Resuming previous conversation. History: &lt;SPAN class=""&gt;{history}&lt;/SPAN&gt;"&lt;/SPAN&gt;&lt;BR /&gt;})&lt;BR /&gt;&lt;SPAN class=""&gt;else&lt;/SPAN&gt;:&lt;BR /&gt;&lt;SPAN class=""&gt;# Brand new session - create fresh Genie conversation&lt;/SPAN&gt;&lt;BR /&gt;new_conv = create_new_conversation()&lt;BR /&gt;state[&lt;SPAN class=""&gt;"conversation_id"&lt;/SPAN&gt;] = new_conv[&lt;SPAN class=""&gt;"conversation_id"&lt;/SPAN&gt;]&lt;BR /&gt;&lt;SPAN class=""&gt;return&lt;/SPAN&gt; state&lt;BR /&gt;&lt;SPAN class=""&gt;def&lt;/SPAN&gt; &lt;SPAN class=""&gt;query_genie_node&lt;/SPAN&gt;(&lt;SPAN class=""&gt;state: AgentState&lt;/SPAN&gt;) -&amp;gt; AgentState:&lt;BR /&gt;&lt;SPAN class=""&gt;"""Send the user's question to Genie with conversation context."""&lt;/SPAN&gt;&lt;BR /&gt;result = query_genie_with_context(&lt;BR /&gt;question=state[&lt;SPAN class=""&gt;"question"&lt;/SPAN&gt;],&lt;BR /&gt;conversation_id=state[&lt;SPAN class=""&gt;"conversation_id"&lt;/SPAN&gt;]&lt;BR /&gt;)&lt;BR /&gt;state[&lt;SPAN class=""&gt;"answer"&lt;/SPAN&gt;] = result.get(&lt;SPAN class=""&gt;"answer"&lt;/SPAN&gt;, &lt;SPAN class=""&gt;"No answer returned"&lt;/SPAN&gt;)&lt;BR /&gt;&lt;SPAN class=""&gt;return&lt;/SPAN&gt; state&lt;BR /&gt;&lt;SPAN class=""&gt;# Build graph&lt;/SPAN&gt;&lt;BR /&gt;workflow = StateGraph(AgentState)&lt;BR /&gt;workflow.add_node(&lt;SPAN class=""&gt;"initialize"&lt;/SPAN&gt;, initialize_session)&lt;BR /&gt;workflow.add_node(&lt;SPAN class=""&gt;"query_genie"&lt;/SPAN&gt;, query_genie_node)&lt;BR /&gt;workflow.set_entry_point(&lt;SPAN class=""&gt;"initialize"&lt;/SPAN&gt;)&lt;BR /&gt;workflow.add_edge(&lt;SPAN class=""&gt;"initialize"&lt;/SPAN&gt;, &lt;SPAN class=""&gt;"query_genie"&lt;/SPAN&gt;)&lt;BR /&gt;workflow.add_edge(&lt;SPAN class=""&gt;"query_genie"&lt;/SPAN&gt;, END)&lt;BR /&gt;&lt;SPAN class=""&gt;# Checkpointer handles STM across steps within the same thread_id&lt;/SPAN&gt;&lt;BR /&gt;checkpointer = MemorySaver()&lt;BR /&gt;graph = workflow.&lt;SPAN class=""&gt;compile&lt;/SPAN&gt;(checkpointer=checkpointer)&lt;BR /&gt;&lt;SPAN class=""&gt;# Run - same thread_id = same session context&lt;/SPAN&gt;&lt;BR /&gt;config = {&lt;SPAN class=""&gt;"configurable"&lt;/SPAN&gt;: {&lt;SPAN class=""&gt;"thread_id"&lt;/SPAN&gt;: &lt;SPAN class=""&gt;"supply-chain-session-001"&lt;/SPAN&gt;}}&lt;BR /&gt;result = graph.invoke(&lt;BR /&gt;{&lt;SPAN class=""&gt;"question"&lt;/SPAN&gt;: &lt;SPAN class=""&gt;"What was our forecast accuracy last quarter?"&lt;/SPAN&gt;,&lt;BR /&gt;&lt;SPAN class=""&gt;"conversation_id"&lt;/SPAN&gt;: &lt;SPAN class=""&gt;"your-persisted-conversation-id-here"&lt;/SPAN&gt;}, &lt;SPAN class=""&gt;# from previous session&lt;/SPAN&gt;&lt;BR /&gt;config=config&lt;BR /&gt;)&lt;BR /&gt;&lt;SPAN class=""&gt;print&lt;/SPAN&gt;(result[&lt;SPAN class=""&gt;"answer"&lt;/SPAN&gt;])&lt;/SPAN&gt;&lt;/PRE&gt;&lt;P class=""&gt;The `thread_id` is what LangGraph uses to resume checkpointed state within a session. The `conversation_id` is what Genie uses to maintain conversation thread continuity. Both work together — neither is sufficient alone.&lt;/P&gt;&lt;P class=""&gt;MCP client wiring&lt;/P&gt;&lt;PRE&gt;&lt;SPAN class=""&gt;&lt;SPAN class=""&gt;from&lt;/SPAN&gt; mcp &lt;SPAN class=""&gt;import&lt;/SPAN&gt; ClientSession, StdioServerParameters&lt;BR /&gt;&lt;SPAN class=""&gt;from&lt;/SPAN&gt; mcp.client.stdio &lt;SPAN class=""&gt;import&lt;/SPAN&gt; stdio_client&lt;BR /&gt;&lt;BR /&gt;server_params = StdioServerParameters(command=&lt;SPAN class=""&gt;"python"&lt;/SPAN&gt;, args=[&lt;SPAN class=""&gt;"genie_mcp_server.py"&lt;/SPAN&gt;])&lt;BR /&gt;&lt;BR /&gt;&lt;SPAN class=""&gt;async&lt;/SPAN&gt; &lt;SPAN class=""&gt;with&lt;/SPAN&gt; stdio_client(server_params) &lt;SPAN class=""&gt;as&lt;/SPAN&gt; (read, write):&lt;BR /&gt;    &lt;SPAN class=""&gt;async&lt;/SPAN&gt; &lt;SPAN class=""&gt;with&lt;/SPAN&gt; ClientSession(read, write) &lt;SPAN class=""&gt;as&lt;/SPAN&gt; session:&lt;BR /&gt;        &lt;SPAN class=""&gt;await&lt;/SPAN&gt; session.initialize()&lt;BR /&gt;        tools = &lt;SPAN class=""&gt;await&lt;/SPAN&gt; session.list_tools() &lt;SPAN class=""&gt;# binds your 3 custom tools&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;/PRE&gt;&lt;P class=""&gt;&lt;STRONG&gt;What This Unlocks vs Managed MCP&lt;/STRONG&gt;&lt;/P&gt;&lt;P class=""&gt;The managed MCP wins on speed of setup. The custom server wins on everything an agentic system actually needs.&lt;/P&gt;&lt;P class=""&gt;&lt;STRONG&gt;Honest Limitations&lt;/STRONG&gt;&lt;/P&gt;&lt;P class=""&gt;Building a custom MCP server doesn’t solve everything. Things that are still unsolved or require additional work:&lt;/P&gt;&lt;P class=""&gt;&lt;STRONG&gt;1. Polling complexity&lt;/STRONG&gt;&lt;/P&gt;&lt;P class=""&gt;Genie is asynchronous. Every query needs a polling loop. Tune the interval and timeout wrong and you either miss results or hit rate limits.&lt;/P&gt;&lt;P class=""&gt;&lt;STRONG&gt;2. conversation_id lifecycle management&lt;/STRONG&gt;&lt;/P&gt;&lt;P class=""&gt;Genie conversations don’t live forever. You need a strategy for when they expire — detect the failure, create a new conversation, and decide how much history to carry forward.&lt;/P&gt;&lt;P class=""&gt;&lt;STRONG&gt;3. SQL consistency is still a Genie problem&lt;/STRONG&gt;&lt;/P&gt;&lt;P class=""&gt;The custom server routes better and maintains context — but Genie’s underlying tendency to generate inconsistent SQL on semantically similar questions is a product limitation, not something the MCP layer can fix.&lt;/P&gt;&lt;P class=""&gt;&lt;STRONG&gt;4. Token cost of history hydration&lt;/STRONG&gt;&lt;/P&gt;&lt;P class=""&gt;Pulling full conversation history at session start adds tokens to every LLM call. For long conversations, this gets expensive quickly. You need a summarization strategy for LTM.&lt;/P&gt;&lt;P class=""&gt;If you’re building something similar or hitting different walls with Genie MCP, drop a comment — happy to compare notes.&lt;BR /&gt;&lt;BR /&gt;# Databricks # MCP #Genie #LangGraph #FastMCP #Agentic AI #Data Engineering&lt;/P&gt;</description>
    <pubDate>Wed, 08 Jul 2026 11:12:26 GMT</pubDate>
    <dc:creator>sagarpgowda7777</dc:creator>
    <dc:date>2026-07-08T11:12:26Z</dc:date>
    <item>
      <title>Building a Custom FastMCP Server on Databricks - What the Managed Genie Can't Do.</title>
      <link>https://community.databricks.com/t5/community-articles/building-a-custom-fastmcp-server-on-databricks-what-the-managed/m-p/162220#M1348</link>
      <description>&lt;P class=""&gt;&lt;STRONG&gt;I Built a Custom MCP Server on Databricks — Here’s What the Managed Version Can’t Do&lt;/STRONG&gt;&lt;/P&gt;&lt;P class=""&gt;&lt;EM&gt;Part 2 of the MCP on Databricks series&lt;/EM&gt;&lt;/P&gt;&lt;P class=""&gt;In Part 1, I covered what Genie MCP and DBSQL MCP give you out of the box — and where they stop.&lt;/P&gt;&lt;P class=""&gt;The short version: both are stateless, the managed Genie MCP has no `conversation_id` control, and the moment you need multi-turn context or cross-session memory, you’ve hit a wall.&lt;/P&gt;&lt;P class=""&gt;This post is about what I built to get past that wall.&lt;/P&gt;&lt;P class=""&gt;&lt;STRONG&gt;The Problems I Set Out to Solve&lt;/STRONG&gt;&lt;/P&gt;&lt;P class=""&gt;After testing the managed Genie MCP in a real agentic workflow, I ran into four specific gaps that couldn’t be patched at the prompt level:&lt;/P&gt;&lt;P class=""&gt;&lt;STRONG&gt;1. No conversation_id control in the per-space Genie MCP&lt;/STRONG&gt;&lt;/P&gt;&lt;P class=""&gt;Every query_space tool call hits a single cross-space endpoint (/api/2.0/mcp/genie/{space_id}) and starts a brand new Genie conversation under the hood — stateless by design. You lose per-space routing, fine-grained tool descriptions, and any sense of session continuity.&lt;/P&gt;&lt;P class=""&gt;&lt;STRONG&gt;2. No cross-session memory&lt;/STRONG&gt;&lt;/P&gt;&lt;P class=""&gt;Even if LangGraph maintains state within a session via its checkpointer, once the session ends, the Genie-side context is gone. There’s nothing to resume from.&lt;/P&gt;&lt;P class=""&gt;&lt;STRONG&gt;3. No custom routing logic&lt;/STRONG&gt;&lt;/P&gt;&lt;P class=""&gt;The managed MCP exposes all Genie spaces as tools but gives you zero control over how the agent picks between them. The tool descriptions are too generic to drive reliable routing.&lt;/P&gt;&lt;P class=""&gt;&lt;STRONG&gt;4. STM lives outside MCP&lt;/STRONG&gt;&lt;/P&gt;&lt;P class=""&gt;Short-term memory is managed at the LangGraph layer, not at the MCP layer. This means the MCP server itself is stateless — each tool call is atomic and knows nothing about previous calls in the same session.&lt;/P&gt;&lt;P class=""&gt;The managed Genie MCP can’t solve any of these. So I built a custom FastMCP server on top of the Genie REST API.&lt;/P&gt;&lt;P class=""&gt;&lt;STRONG&gt;Architecture&lt;/STRONG&gt;&lt;/P&gt;&lt;P class=""&gt;Before diving into the code, here’s how the pieces fit together:&lt;/P&gt;&lt;DIV class=""&gt;&lt;SPAN class=""&gt;Press enter or click to view image in full size&lt;/SPAN&gt;&lt;DIV class=""&gt;&lt;span class="lia-inline-image-display-wrapper lia-image-align-inline" image-alt="sagarpgowda7777_0-1783508005850.jpeg" style="width: 400px;"&gt;&lt;img src="https://community.databricks.com/t5/image/serverpage/image-id/28734iC888F3A40F966F05/image-size/medium?v=v2&amp;amp;px=400" role="button" title="sagarpgowda7777_0-1783508005850.jpeg" alt="sagarpgowda7777_0-1783508005850.jpeg" /&gt;&lt;/span&gt;&lt;P&gt;&amp;nbsp;&lt;/P&gt;&lt;/DIV&gt;&lt;/DIV&gt;&lt;P class=""&gt;The key insight: `conversation_id` is the bridge between LangGraph state and Genie’s conversation thread. Persist it, and you get continuity. Drop it, and you’re back to square one every call.&lt;/P&gt;&lt;P class=""&gt;&lt;STRONG&gt;Building the Custom FastMCP Server&lt;/STRONG&gt;&lt;/P&gt;&lt;P class=""&gt;&lt;STRONG&gt;Step 1 — Basic Server Skeleton&lt;/STRONG&gt;&lt;/P&gt;&lt;PRE&gt;&lt;SPAN class=""&gt;&lt;SPAN class=""&gt;from&lt;/SPAN&gt; fastmcp &lt;SPAN class=""&gt;import&lt;/SPAN&gt; FastMCP&lt;BR /&gt;&lt;SPAN class=""&gt;import&lt;/SPAN&gt; requests&lt;BR /&gt;&lt;SPAN class=""&gt;import&lt;/SPAN&gt; os&lt;BR /&gt;&lt;SPAN class=""&gt;import&lt;/SPAN&gt; time&lt;BR /&gt;mcp = FastMCP(&lt;SPAN class=""&gt;"databricks-genie-mcp"&lt;/SPAN&gt;)&lt;BR /&gt;DATABRICKS_HOST = os.environ[&lt;SPAN class=""&gt;"DATABRICKS_HOST"&lt;/SPAN&gt;]&lt;BR /&gt;DATABRICKS_TOKEN = os.environ[&lt;SPAN class=""&gt;"DATABRICKS_TOKEN"&lt;/SPAN&gt;]&lt;BR /&gt;GENIE_SPACE_ID = os.environ[&lt;SPAN class=""&gt;"GENIE_SPACE_ID"&lt;/SPAN&gt;]&lt;BR /&gt;HEADERS = {&lt;BR /&gt;&lt;SPAN class=""&gt;"Authorization"&lt;/SPAN&gt;: &lt;SPAN class=""&gt;f"Bearer &lt;SPAN class=""&gt;{DATABRICKS_TOKEN}&lt;/SPAN&gt;"&lt;/SPAN&gt;,&lt;BR /&gt;&lt;SPAN class=""&gt;"Content-Type"&lt;/SPAN&gt;: &lt;SPAN class=""&gt;"application/json"&lt;/SPAN&gt;&lt;BR /&gt;}&lt;BR /&gt;&lt;SPAN class=""&gt;def&lt;/SPAN&gt; &lt;SPAN class=""&gt;genie_api&lt;/SPAN&gt;(&lt;SPAN class=""&gt;endpoint: &lt;SPAN class=""&gt;str&lt;/SPAN&gt;, method: &lt;SPAN class=""&gt;str&lt;/SPAN&gt; = &lt;SPAN class=""&gt;"GET"&lt;/SPAN&gt;, payload: &lt;SPAN class=""&gt;dict&lt;/SPAN&gt; = &lt;SPAN class=""&gt;None):&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;BR /&gt;    url = &lt;SPAN class=""&gt;f"&lt;SPAN class=""&gt;{DATABRICKS_HOST}&lt;/SPAN&gt;/api/2.0/genie/spaces/&lt;SPAN class=""&gt;{GENIE_SPACE_ID}&lt;/SPAN&gt;/&lt;SPAN class=""&gt;{endpoint}&lt;/SPAN&gt;"&lt;/SPAN&gt;&lt;BR /&gt;    response = requests.request(method, url, headers=HEADERS, json=payload)&lt;BR /&gt;    response.raise_for_status()&lt;BR /&gt;&lt;SPAN class=""&gt;    return&lt;/SPAN&gt; response.json()&lt;/SPAN&gt;&lt;/PRE&gt;&lt;P class=""&gt;Nothing surprising here — FastMCP server wrapping the Genie REST API with a shared helper for authenticated requests.&lt;/P&gt;&lt;P class=""&gt;&lt;STRONG&gt;Step 2 — The Core: conversation_id Persistence&lt;/STRONG&gt;&lt;/P&gt;&lt;P class=""&gt;This is the piece the managed Genie MCP simply doesn’t give you.&lt;/P&gt;&lt;PRE&gt;&lt;SPAN class=""&gt;&lt;SPAN class=""&gt;@mcp.tool()&lt;/SPAN&gt;&lt;BR /&gt;&lt;SPAN class=""&gt;def&lt;/SPAN&gt; &lt;SPAN class=""&gt;create_new_conversation&lt;/SPAN&gt;() -&amp;gt; &lt;SPAN class=""&gt;dict&lt;/SPAN&gt;:&lt;BR /&gt;&lt;SPAN class=""&gt;"""&lt;BR /&gt;Start a new Genie conversation and return the conversation_id.&lt;BR /&gt;Store this in LangGraph state to enable multi-turn context.&lt;BR /&gt;"""&lt;/SPAN&gt;&lt;BR /&gt;result = genie_api(&lt;BR /&gt;&lt;SPAN class=""&gt;"start-conversation"&lt;/SPAN&gt;,&lt;BR /&gt;method=&lt;SPAN class=""&gt;"POST"&lt;/SPAN&gt;,&lt;BR /&gt;payload={&lt;SPAN class=""&gt;"content"&lt;/SPAN&gt;: &lt;SPAN class=""&gt;"Session initialized."&lt;/SPAN&gt;}&lt;BR /&gt;)&lt;BR /&gt;&lt;SPAN class=""&gt;return&lt;/SPAN&gt; {&lt;BR /&gt;&lt;SPAN class=""&gt;"conversation_id"&lt;/SPAN&gt;: result[&lt;SPAN class=""&gt;"conversation"&lt;/SPAN&gt;][&lt;SPAN class=""&gt;"conversation_id"&lt;/SPAN&gt;],&lt;BR /&gt;&lt;SPAN class=""&gt;"initial_message_id"&lt;/SPAN&gt;: result[&lt;SPAN class=""&gt;"message"&lt;/SPAN&gt;][&lt;SPAN class=""&gt;"message_id"&lt;/SPAN&gt;],&lt;BR /&gt;&lt;SPAN class=""&gt;"message"&lt;/SPAN&gt;: &lt;SPAN class=""&gt;"New conversation created. Pass this conversation_id&lt;BR /&gt;to all subsequent query_genie calls."&lt;/SPAN&gt;&lt;BR /&gt;}&lt;BR /&gt;&lt;SPAN class=""&gt;@mcp.tool()&lt;/SPAN&gt;&lt;BR /&gt;&lt;SPAN class=""&gt;def&lt;/SPAN&gt; &lt;SPAN class=""&gt;query_genie_with_context&lt;/SPAN&gt;(&lt;SPAN class=""&gt;question: &lt;SPAN class=""&gt;str&lt;/SPAN&gt;, conversation_id: &lt;SPAN class=""&gt;str&lt;/SPAN&gt;&lt;/SPAN&gt;) -&amp;gt; &lt;SPAN class=""&gt;dict&lt;/SPAN&gt;:&lt;BR /&gt;&lt;SPAN class=""&gt;"""&lt;BR /&gt;Query Genie within an existing conversation thread.&lt;BR /&gt;Requires conversation_id from a previous create_new_conversation call.&lt;BR /&gt;"""&lt;/SPAN&gt;&lt;BR /&gt;&lt;SPAN class=""&gt;# Send message to existing conversation&lt;/SPAN&gt;&lt;BR /&gt;message_result = genie_api(&lt;BR /&gt;&lt;SPAN class=""&gt;f"conversations/&lt;SPAN class=""&gt;{conversation_id}&lt;/SPAN&gt;/messages"&lt;/SPAN&gt;,&lt;BR /&gt;method=&lt;SPAN class=""&gt;"POST"&lt;/SPAN&gt;,&lt;BR /&gt;payload={&lt;SPAN class=""&gt;"content"&lt;/SPAN&gt;: question}&lt;BR /&gt;)&lt;BR /&gt;message_id = message_result[&lt;SPAN class=""&gt;"message_id"&lt;/SPAN&gt;]&lt;/SPAN&gt;&lt;/PRE&gt;&lt;P class=""&gt;Poll for result (Genie is async)&lt;/P&gt;&lt;PRE&gt;&lt;SPAN class=""&gt;&lt;SPAN class=""&gt;for&lt;/SPAN&gt; _ &lt;SPAN class=""&gt;in&lt;/SPAN&gt; &lt;SPAN class=""&gt;range&lt;/SPAN&gt;(&lt;SPAN class=""&gt;30):&lt;/SPAN&gt;&lt;BR /&gt;status = genie_api(&lt;BR /&gt;&lt;SPAN class=""&gt;f"conversations/&lt;SPAN class=""&gt;{conversation_id}&lt;/SPAN&gt;/messages/&lt;SPAN class=""&gt;{message_id}&lt;/SPAN&gt;"&lt;/SPAN&gt;&lt;BR /&gt;)&lt;BR /&gt;&lt;SPAN class=""&gt;if&lt;/SPAN&gt; status[&lt;SPAN class=""&gt;"status"&lt;/SPAN&gt;] == &lt;SPAN class=""&gt;"COMPLETED"&lt;/SPAN&gt;:&lt;BR /&gt;&lt;SPAN class=""&gt;return&lt;/SPAN&gt; {&lt;BR /&gt;&lt;SPAN class=""&gt;"answer"&lt;/SPAN&gt;: status.get(&lt;SPAN class=""&gt;"content"&lt;/SPAN&gt;, &lt;SPAN class=""&gt;""&lt;/SPAN&gt;),&lt;BR /&gt;&lt;SPAN class=""&gt;"conversation_id"&lt;/SPAN&gt;: conversation_id,&lt;BR /&gt;&lt;SPAN class=""&gt;"message_id"&lt;/SPAN&gt;: message_id&lt;BR /&gt;}&lt;BR /&gt;&lt;SPAN class=""&gt;elif&lt;/SPAN&gt; status[&lt;SPAN class=""&gt;"status"&lt;/SPAN&gt;] == &lt;SPAN class=""&gt;"FAILED"&lt;/SPAN&gt;:&lt;BR /&gt;&lt;SPAN class=""&gt;return&lt;/SPAN&gt; {&lt;SPAN class=""&gt;"error"&lt;/SPAN&gt;: &lt;SPAN class=""&gt;"Genie query failed"&lt;/SPAN&gt;, &lt;SPAN class=""&gt;"conversation_id"&lt;/SPAN&gt;: conversation_id}&lt;BR /&gt;time.sleep(&lt;SPAN class=""&gt;2&lt;/SPAN&gt;)&lt;BR /&gt;&lt;SPAN class=""&gt;return&lt;/SPAN&gt; {&lt;SPAN class=""&gt;"error"&lt;/SPAN&gt;: &lt;SPAN class=""&gt;"Timeout waiting for Genie response"&lt;/SPAN&gt;}&lt;/SPAN&gt;&lt;/PRE&gt;&lt;P class=""&gt;The critical difference from the managed MCP: `conversation_id` is passed explicitly on every call. Genie sees this as a continuation of the same conversation — it has full context of everything said before.&lt;/P&gt;&lt;P class=""&gt;&lt;STRONG&gt;Step 3 — Cross-Session Memory via History Retrieval&lt;/STRONG&gt;&lt;/P&gt;&lt;P class=""&gt;This is the tool the managed Genie MCP has no equivalent for.&lt;/P&gt;&lt;PRE&gt;&lt;SPAN class=""&gt;&lt;SPAN class=""&gt;@mcp.tool()&lt;/SPAN&gt;&lt;BR /&gt;&lt;SPAN class=""&gt;def&lt;/SPAN&gt; &lt;SPAN class=""&gt;get_conversation_history&lt;/SPAN&gt;(&lt;SPAN class=""&gt;conversation_id: &lt;SPAN class=""&gt;str&lt;/SPAN&gt;&lt;/SPAN&gt;) -&amp;gt; &lt;SPAN class=""&gt;dict&lt;/SPAN&gt;:&lt;BR /&gt;&lt;SPAN class=""&gt;"""&lt;BR /&gt;Retrieve full message history from a previous Genie conversation.&lt;BR /&gt;Use this at the start of a new session to hydrate context.&lt;BR /&gt;"""&lt;/SPAN&gt;&lt;BR /&gt;result = genie_api(&lt;SPAN class=""&gt;f"conversations/&lt;SPAN class=""&gt;{conversation_id}&lt;/SPAN&gt;/messages"&lt;/SPAN&gt;)&lt;BR /&gt;messages = []&lt;BR /&gt;&lt;SPAN class=""&gt;for&lt;/SPAN&gt; msg &lt;SPAN class=""&gt;in&lt;/SPAN&gt; result.get(&lt;SPAN class=""&gt;"messages"&lt;/SPAN&gt;, []):&lt;BR /&gt;messages.append({&lt;BR /&gt;&lt;SPAN class=""&gt;"role"&lt;/SPAN&gt;: msg.get(&lt;SPAN class=""&gt;"role"&lt;/SPAN&gt;),&lt;BR /&gt;&lt;SPAN class=""&gt;"content"&lt;/SPAN&gt;: msg.get(&lt;SPAN class=""&gt;"content"&lt;/SPAN&gt;),&lt;BR /&gt;&lt;SPAN class=""&gt;"timestamp"&lt;/SPAN&gt;: msg.get(&lt;SPAN class=""&gt;"created_at"&lt;/SPAN&gt;)&lt;BR /&gt;})&lt;BR /&gt;&lt;SPAN class=""&gt;return&lt;/SPAN&gt; {&lt;BR /&gt;&lt;SPAN class=""&gt;"conversation_id"&lt;/SPAN&gt;: conversation_id,&lt;BR /&gt;&lt;SPAN class=""&gt;"message_count"&lt;/SPAN&gt;: &lt;SPAN class=""&gt;len&lt;/SPAN&gt;(messages),&lt;BR /&gt;&lt;SPAN class=""&gt;"history"&lt;/SPAN&gt;: messages&lt;BR /&gt;}&lt;/SPAN&gt;&lt;/PRE&gt;&lt;P class=""&gt;At the start of a new session, the LangGraph agent calls `get_conversation_history()` with the persisted `conversation_id` from the previous session. It hydrates the context before asking anything new.&lt;/P&gt;&lt;P class=""&gt;This is what makes the system behave like it has memory — even though MCP itself is stateless.&lt;/P&gt;&lt;P class=""&gt;&lt;STRONG&gt;Wiring It Into LangGraph&lt;/STRONG&gt;&lt;/P&gt;&lt;P class=""&gt;&lt;STRONG&gt;STM vs LTM — Clarifying the Layers&lt;/STRONG&gt;&lt;/P&gt;&lt;P class=""&gt;LangGraph handles STM. The persisted `conversation_id` bridges LTM. The custom MCP server connects both to Genie.&lt;/P&gt;&lt;P class=""&gt;&lt;STRONG&gt;The LangGraph Graph&lt;/STRONG&gt;&lt;/P&gt;&lt;PRE&gt;&lt;SPAN class=""&gt;&lt;SPAN class=""&gt;from&lt;/SPAN&gt; langgraph.graph &lt;SPAN class=""&gt;import&lt;/SPAN&gt; StateGraph, END&lt;BR /&gt;&lt;SPAN class=""&gt;from&lt;/SPAN&gt; langgraph.checkpoint.memory &lt;SPAN class=""&gt;import&lt;/SPAN&gt; MemorySaver&lt;BR /&gt;&lt;SPAN class=""&gt;from&lt;/SPAN&gt; langchain_openai &lt;SPAN class=""&gt;import&lt;/SPAN&gt; ChatOpenAI&lt;BR /&gt;&lt;SPAN class=""&gt;from&lt;/SPAN&gt; typing &lt;SPAN class=""&gt;import&lt;/SPAN&gt; TypedDict, &lt;SPAN class=""&gt;Optional&lt;/SPAN&gt;&lt;BR /&gt;&lt;SPAN class=""&gt;class&lt;/SPAN&gt; &lt;SPAN class=""&gt;AgentState&lt;/SPAN&gt;(&lt;SPAN class=""&gt;TypedDict):&lt;/SPAN&gt;&lt;BR /&gt;messages: &lt;SPAN class=""&gt;list&lt;/SPAN&gt;&lt;BR /&gt;conversation_id: &lt;SPAN class=""&gt;Optional&lt;/SPAN&gt;[&lt;SPAN class=""&gt;str&lt;/SPAN&gt;]  &lt;SPAN class=""&gt;# Genie conversation_id persisted here&lt;/SPAN&gt;&lt;BR /&gt;question: &lt;SPAN class=""&gt;str&lt;/SPAN&gt;&lt;BR /&gt;answer: &lt;SPAN class=""&gt;Optional&lt;/SPAN&gt;[&lt;SPAN class=""&gt;str&lt;/SPAN&gt;]&lt;BR /&gt;llm = ChatOpenAI(&lt;BR /&gt;base_url=&lt;SPAN class=""&gt;f"&lt;SPAN class=""&gt;{host}&lt;/SPAN&gt;/serving-endpoints"&lt;/SPAN&gt;,&lt;BR /&gt;api_key=user_token,&lt;BR /&gt;model=LLM_ENDPOINT,&lt;BR /&gt;temperature=&lt;SPAN class=""&gt;0&lt;/SPAN&gt;,&lt;BR /&gt;)&lt;BR /&gt;&lt;SPAN class=""&gt;def&lt;/SPAN&gt; &lt;SPAN class=""&gt;initialize_session&lt;/SPAN&gt;(&lt;SPAN class=""&gt;state: AgentState&lt;/SPAN&gt;) -&amp;gt; AgentState:&lt;BR /&gt;&lt;SPAN class=""&gt;"""&lt;BR /&gt;At session start - either create new Genie conversation&lt;BR /&gt;or retrieve history from a previous one.&lt;BR /&gt;"""&lt;/SPAN&gt;&lt;BR /&gt;&lt;SPAN class=""&gt;if&lt;/SPAN&gt; state.get(&lt;SPAN class=""&gt;"conversation_id"):&lt;/SPAN&gt;&lt;BR /&gt;&lt;SPAN class=""&gt;# Resume existing conversation - hydrate context&lt;/SPAN&gt;&lt;BR /&gt;history = get_conversation_history(state[&lt;SPAN class=""&gt;"conversation_id"&lt;/SPAN&gt;])&lt;BR /&gt;state[&lt;SPAN class=""&gt;"messages"&lt;/SPAN&gt;].append({&lt;BR /&gt;&lt;SPAN class=""&gt;"role"&lt;/SPAN&gt;: &lt;SPAN class=""&gt;"system"&lt;/SPAN&gt;,&lt;BR /&gt;&lt;SPAN class=""&gt;"content"&lt;/SPAN&gt;: &lt;SPAN class=""&gt;f"Resuming previous conversation. History: &lt;SPAN class=""&gt;{history}&lt;/SPAN&gt;"&lt;/SPAN&gt;&lt;BR /&gt;})&lt;BR /&gt;&lt;SPAN class=""&gt;else&lt;/SPAN&gt;:&lt;BR /&gt;&lt;SPAN class=""&gt;# Brand new session - create fresh Genie conversation&lt;/SPAN&gt;&lt;BR /&gt;new_conv = create_new_conversation()&lt;BR /&gt;state[&lt;SPAN class=""&gt;"conversation_id"&lt;/SPAN&gt;] = new_conv[&lt;SPAN class=""&gt;"conversation_id"&lt;/SPAN&gt;]&lt;BR /&gt;&lt;SPAN class=""&gt;return&lt;/SPAN&gt; state&lt;BR /&gt;&lt;SPAN class=""&gt;def&lt;/SPAN&gt; &lt;SPAN class=""&gt;query_genie_node&lt;/SPAN&gt;(&lt;SPAN class=""&gt;state: AgentState&lt;/SPAN&gt;) -&amp;gt; AgentState:&lt;BR /&gt;&lt;SPAN class=""&gt;"""Send the user's question to Genie with conversation context."""&lt;/SPAN&gt;&lt;BR /&gt;result = query_genie_with_context(&lt;BR /&gt;question=state[&lt;SPAN class=""&gt;"question"&lt;/SPAN&gt;],&lt;BR /&gt;conversation_id=state[&lt;SPAN class=""&gt;"conversation_id"&lt;/SPAN&gt;]&lt;BR /&gt;)&lt;BR /&gt;state[&lt;SPAN class=""&gt;"answer"&lt;/SPAN&gt;] = result.get(&lt;SPAN class=""&gt;"answer"&lt;/SPAN&gt;, &lt;SPAN class=""&gt;"No answer returned"&lt;/SPAN&gt;)&lt;BR /&gt;&lt;SPAN class=""&gt;return&lt;/SPAN&gt; state&lt;BR /&gt;&lt;SPAN class=""&gt;# Build graph&lt;/SPAN&gt;&lt;BR /&gt;workflow = StateGraph(AgentState)&lt;BR /&gt;workflow.add_node(&lt;SPAN class=""&gt;"initialize"&lt;/SPAN&gt;, initialize_session)&lt;BR /&gt;workflow.add_node(&lt;SPAN class=""&gt;"query_genie"&lt;/SPAN&gt;, query_genie_node)&lt;BR /&gt;workflow.set_entry_point(&lt;SPAN class=""&gt;"initialize"&lt;/SPAN&gt;)&lt;BR /&gt;workflow.add_edge(&lt;SPAN class=""&gt;"initialize"&lt;/SPAN&gt;, &lt;SPAN class=""&gt;"query_genie"&lt;/SPAN&gt;)&lt;BR /&gt;workflow.add_edge(&lt;SPAN class=""&gt;"query_genie"&lt;/SPAN&gt;, END)&lt;BR /&gt;&lt;SPAN class=""&gt;# Checkpointer handles STM across steps within the same thread_id&lt;/SPAN&gt;&lt;BR /&gt;checkpointer = MemorySaver()&lt;BR /&gt;graph = workflow.&lt;SPAN class=""&gt;compile&lt;/SPAN&gt;(checkpointer=checkpointer)&lt;BR /&gt;&lt;SPAN class=""&gt;# Run - same thread_id = same session context&lt;/SPAN&gt;&lt;BR /&gt;config = {&lt;SPAN class=""&gt;"configurable"&lt;/SPAN&gt;: {&lt;SPAN class=""&gt;"thread_id"&lt;/SPAN&gt;: &lt;SPAN class=""&gt;"supply-chain-session-001"&lt;/SPAN&gt;}}&lt;BR /&gt;result = graph.invoke(&lt;BR /&gt;{&lt;SPAN class=""&gt;"question"&lt;/SPAN&gt;: &lt;SPAN class=""&gt;"What was our forecast accuracy last quarter?"&lt;/SPAN&gt;,&lt;BR /&gt;&lt;SPAN class=""&gt;"conversation_id"&lt;/SPAN&gt;: &lt;SPAN class=""&gt;"your-persisted-conversation-id-here"&lt;/SPAN&gt;}, &lt;SPAN class=""&gt;# from previous session&lt;/SPAN&gt;&lt;BR /&gt;config=config&lt;BR /&gt;)&lt;BR /&gt;&lt;SPAN class=""&gt;print&lt;/SPAN&gt;(result[&lt;SPAN class=""&gt;"answer"&lt;/SPAN&gt;])&lt;/SPAN&gt;&lt;/PRE&gt;&lt;P class=""&gt;The `thread_id` is what LangGraph uses to resume checkpointed state within a session. The `conversation_id` is what Genie uses to maintain conversation thread continuity. Both work together — neither is sufficient alone.&lt;/P&gt;&lt;P class=""&gt;MCP client wiring&lt;/P&gt;&lt;PRE&gt;&lt;SPAN class=""&gt;&lt;SPAN class=""&gt;from&lt;/SPAN&gt; mcp &lt;SPAN class=""&gt;import&lt;/SPAN&gt; ClientSession, StdioServerParameters&lt;BR /&gt;&lt;SPAN class=""&gt;from&lt;/SPAN&gt; mcp.client.stdio &lt;SPAN class=""&gt;import&lt;/SPAN&gt; stdio_client&lt;BR /&gt;&lt;BR /&gt;server_params = StdioServerParameters(command=&lt;SPAN class=""&gt;"python"&lt;/SPAN&gt;, args=[&lt;SPAN class=""&gt;"genie_mcp_server.py"&lt;/SPAN&gt;])&lt;BR /&gt;&lt;BR /&gt;&lt;SPAN class=""&gt;async&lt;/SPAN&gt; &lt;SPAN class=""&gt;with&lt;/SPAN&gt; stdio_client(server_params) &lt;SPAN class=""&gt;as&lt;/SPAN&gt; (read, write):&lt;BR /&gt;    &lt;SPAN class=""&gt;async&lt;/SPAN&gt; &lt;SPAN class=""&gt;with&lt;/SPAN&gt; ClientSession(read, write) &lt;SPAN class=""&gt;as&lt;/SPAN&gt; session:&lt;BR /&gt;        &lt;SPAN class=""&gt;await&lt;/SPAN&gt; session.initialize()&lt;BR /&gt;        tools = &lt;SPAN class=""&gt;await&lt;/SPAN&gt; session.list_tools() &lt;SPAN class=""&gt;# binds your 3 custom tools&lt;/SPAN&gt;&lt;/SPAN&gt;&lt;/PRE&gt;&lt;P class=""&gt;&lt;STRONG&gt;What This Unlocks vs Managed MCP&lt;/STRONG&gt;&lt;/P&gt;&lt;P class=""&gt;The managed MCP wins on speed of setup. The custom server wins on everything an agentic system actually needs.&lt;/P&gt;&lt;P class=""&gt;&lt;STRONG&gt;Honest Limitations&lt;/STRONG&gt;&lt;/P&gt;&lt;P class=""&gt;Building a custom MCP server doesn’t solve everything. Things that are still unsolved or require additional work:&lt;/P&gt;&lt;P class=""&gt;&lt;STRONG&gt;1. Polling complexity&lt;/STRONG&gt;&lt;/P&gt;&lt;P class=""&gt;Genie is asynchronous. Every query needs a polling loop. Tune the interval and timeout wrong and you either miss results or hit rate limits.&lt;/P&gt;&lt;P class=""&gt;&lt;STRONG&gt;2. conversation_id lifecycle management&lt;/STRONG&gt;&lt;/P&gt;&lt;P class=""&gt;Genie conversations don’t live forever. You need a strategy for when they expire — detect the failure, create a new conversation, and decide how much history to carry forward.&lt;/P&gt;&lt;P class=""&gt;&lt;STRONG&gt;3. SQL consistency is still a Genie problem&lt;/STRONG&gt;&lt;/P&gt;&lt;P class=""&gt;The custom server routes better and maintains context — but Genie’s underlying tendency to generate inconsistent SQL on semantically similar questions is a product limitation, not something the MCP layer can fix.&lt;/P&gt;&lt;P class=""&gt;&lt;STRONG&gt;4. Token cost of history hydration&lt;/STRONG&gt;&lt;/P&gt;&lt;P class=""&gt;Pulling full conversation history at session start adds tokens to every LLM call. For long conversations, this gets expensive quickly. You need a summarization strategy for LTM.&lt;/P&gt;&lt;P class=""&gt;If you’re building something similar or hitting different walls with Genie MCP, drop a comment — happy to compare notes.&lt;BR /&gt;&lt;BR /&gt;# Databricks # MCP #Genie #LangGraph #FastMCP #Agentic AI #Data Engineering&lt;/P&gt;</description>
      <pubDate>Wed, 08 Jul 2026 11:12:26 GMT</pubDate>
      <guid>https://community.databricks.com/t5/community-articles/building-a-custom-fastmcp-server-on-databricks-what-the-managed/m-p/162220#M1348</guid>
      <dc:creator>sagarpgowda7777</dc:creator>
      <dc:date>2026-07-08T11:12:26Z</dc:date>
    </item>
  </channel>
</rss>

