> ## Documentation Index
> Fetch the complete documentation index at: https://notes.kodekloud.com/llms.txt
> Use this file to discover all available pages before exploring further.

# LangChain Agent Shared Wiring

> Guide to shared Python module wiring for LangChain agents using MCP tools, covering discovery, LLM setup, agent creation, invocation, and cleanup

Hello and welcome back.

This guide explains the shared wiring used when creating LangChain agents that call MCP tools. We use a single reusable Python module that handles discovery, LLM construction, agent creation, invocation, and cleanup. Below you’ll find the shell commands used to expose the cluster kubeconfig and run the MCP container locally, the full `mcp_client.py` module, and a step-by-step explanation of each section so you can see how the pieces fit together.

Shell (context)

```sh theme={null}
kind get kubeconfig --name finops-cluster --internal > /tmp/kubeconfig-docker.yaml

docker run --rm -p 8000:8000 --network kind \
  -v /tmp/kubeconfig-docker.yaml:/home/appuser/.kube/config:ro \
  -e ENABLE_UNSAFE_STREAMABLE_HTTP_TRANSPORT=1 \
  -e PORT=8000 \
  <other-docker-args>
```

Full shared module: `mcp_client.py`

```python theme={null}
"""Shared LangChain + MCP helpers for example scripts.

Expects a persistent HTTP MCP container (native Streamable HTTP on
http://localhost:8000/mcp). No Supergateway required.
"""

import json
import os
from pathlib import Path
from typing import Any

from dotenv import load_dotenv
from langchain.agents import create_agent
from langchain_core.messages import HumanMessage
from langchain_core.tools import BaseTool
from langchain_openai import ChatOpenAI
from langchain_mcp_tools import convert_mcp_to_langchain_tools

# Load the .env file located at the project root (reached here via Path(__file__).parents[3])
load_dotenv(Path(__file__).parents[3] / ".env")

MCP_URL = os.getenv("K8S_MCP_URL", "http://localhost:8000/mcp")
MCP_SERVERS = {
    "k8s": {
        "url": MCP_URL,
        "transport": "streamable_http",
    }
}


def decode_tool_result(result: Any) -> dict:
    """Normalize MCP tool output to a dictionary.

    MCP tools may return a dict already or a JSON string. This helper
    converts either shape into a dict with a predictable structure.
    """
    if isinstance(result, dict):
        return result
    text = str(result).strip()
    if not text:
        return {"items": []}
    try:
        return json.loads(text)
    except json.JSONDecodeError:
        return {"items": []}


def tool_by_name(tools: list[BaseTool], name: str) -> BaseTool:
    """Find a LangChain tool by name in the list returned from MCP."""
    tool = next((t for t in tools if t.name == name), None)
    if tool is None:
        raise RuntimeError(f"MCP tool not found: {name}")
    return tool


async def get_mcp_tools():
    """Convert MCP-discovered tools into LangChain tools.

    Returns a tuple (tools, cleanup) where `cleanup` should be awaited
    when tools are no longer needed.
    """
    return await convert_mcp_to_langchain_tools(MCP_SERVERS)


def build_llm(max_tokens: int | None = None) -> ChatOpenAI:
    """Construct the ChatOpenAI LLM from environment variables."""
    return ChatOpenAI(
        model=os.environ["OPENAI_MODEL_ID"],
        base_url=os.environ["OPENAI_BASE_URL"],
        api_key=os.environ["OPENAI_API_KEY"],
        temperature=float(os.getenv("OPENAI_TEMPERATURE", "0.3")),
        max_tokens=max_tokens or int(os.getenv("OPENAI_MAX_TOKENS", "512")),
    )


async def run_agent(prompt: str, max_tokens: int | None = None) -> str:
    """Full agent loop: discover tools, create the agent, invoke it, return final message.

    This function:
    - Converts MCP tools to LangChain tools
    - Builds the LLM using environment variables
    - Creates a LangChain agent with the tools
    - Invokes the agent with a single HumanMessage containing the prompt
    - Returns the last assistant message content
    - Ensures cleanup is always run
    """
    tools, cleanup = await get_mcp_tools()
    try:
        agent = create_agent(build_llm(max_tokens=max_tokens), tools)
        result = await agent.ainvoke({"messages": [HumanMessage(content=prompt)]})
        return result["messages"][-1].content
    finally:
        await cleanup()
```

Key imports and what they do

* Standard library: `json`, `os`, `Path`, and `typing.Any` for I/O, environment handling, and type hints.
* `dotenv.load_dotenv`: loads environment variables from a `.env` file at the project root (via `Path(__file__).parents[3]`).
* LangChain:
  * `create_agent` — build the agent with an LLM and available tools.
  * `HumanMessage` / `BaseTool` — message and tool abstractions used by LangChain agents.
* LLM client: `ChatOpenAI` — a wrapper compatible with OpenAI-style LLM endpoints.
* MCP tool conversion: `convert_mcp_to_langchain_tools` — performs the MCP handshake and returns LangChain-ready tool objects so you don’t have to manually define tool schemas.

Loading configuration from .env

* The module expects a `.env` at your project root with values required to construct the LLM client. Typical variables:
  * `OPENAI_MODEL_ID`
  * `OPENAI_BASE_URL`
  * `OPENAI_API_KEY`
  * Optional tuning: `OPENAI_TEMPERATURE`, `OPENAI_MAX_TOKENS`
* These are read by `build_llm()` to initialize `ChatOpenAI`.

Do not commit your real `.env` to source control. The `.env` contains secrets (API keys) and should remain local or be injected via secure CI/CD secret management.

Environment variables (at-a-glance)

| Variable | Purpose | Example |
| - | -: | - |
| `OPENAI_MODEL_ID` | Model identifier used by `ChatOpenAI` | `gpt-4o-mini` |
| `OPENAI_BASE_URL` | Base URL for the OpenAI-compatible endpoint | `https://api.example.com/v1` |
| `OPENAI_API_KEY` | API key used to authenticate requests | `sk-...` |
| `OPENAI_TEMPERATURE` | Optional; sampling temperature | `0.3` |
| `OPENAI_MAX_TOKENS` | Optional; default maximum tokens for responses | `512` |
| `K8S_MCP_URL` | Optional; override MCP URL (defaults to `http://localhost:8000/mcp`) | `http://mcp.example.com/mcp` |

MCP URL and transport

* `MCP_URL` defaults to `http://localhost:8000/mcp` to match the locally run MCP container. Override with `K8S_MCP_URL` as needed.
* `MCP_SERVERS` sets up the server description used by `convert_mcp_to_langchain_tools` and specifies `transport: "streamable_http"` (the transport used by the MCP container in this setup).

Why convert MCP tools to LangChain tools?

* `convert_mcp_to_langchain_tools` performs discovery/handshake with MCP and returns tool objects that the LangChain agent understands. This removes manual tool wiring and keeps your application focused on prompts and orchestration.

Helper: normalize tool output — `decode_tool_result`

* MCP tool outputs vary: some return Python dictionaries, others return JSON strings. `decode_tool_result` normalizes either format into a predictable `dict` with a safe fallback `{"items": []}` for empty/invalid outputs. Use this when consuming raw tool results to simplify downstream logic.

Helper: look up a tool — `tool_by_name`

* Simple utility that searches a list of LangChain tools by `.name`. It raises a clear `RuntimeError` if the requested tool isn’t available, making debugging easier when a tool is missing.

Getting MCP tools — `get_mcp_tools`

* Calls `convert_mcp_to_langchain_tools(MCP_SERVERS)` and returns `(tools, cleanup)`. The `cleanup` coroutine should be awaited when you’re finished with the tools to clean up any background connections.

Building the LLM — `build_llm`

* Creates a configured `ChatOpenAI` instance from environment variables. You can override `max_tokens` when calling this function or `run_agent`.

Core agent loop — `run_agent`
This function encapsulates the minimal, robust loop for an agent-based call:

1. Discover MCP tools and obtain a `cleanup` coroutine.
2. Build the LLM using `build_llm`.
3. Create a LangChain agent with `create_agent`.
4. Invoke the agent using `agent.ainvoke(...)` with a single `HumanMessage` containing your prompt.
5. Return the agent’s final assistant message content.
6. Ensure `cleanup()` is awaited in a `finally` block so resources are always released.

Usage pattern: where to write your prompt

* Call `run_agent(prompt, max_tokens=...)` from your application. The shared client handles discovery, LLM construction, agent orchestration, invocation, and cleanup. Your application code only needs to compose the user-facing prompt.

Simple async example

```python theme={null}
import asyncio
from mcp_client import run_agent

async def main():
    prompt = "List all running pods in the finops namespace and summarize any errors."
    output = await run_agent(prompt, max_tokens=256)
    print(output)

asyncio.run(main())
```

Common troubleshooting tips

* If tools are not discovered, verify `K8S_MCP_URL` and that the MCP container is reachable.
* Ensure your `.env` variables match the expected names and values; missing or malformed keys will cause `build_llm()` to fail.
* Use `tool_by_name(tools, "tool-name")` to validate that a specific tool exists before attempting to call it.

Next steps

* The follow-up article will demonstrate a concise prompt that uses these helpers to call MCP tools and produce concrete answers. It will include examples of interpreting tool outputs and combining multiple tool calls in an agent workflow.

Links and references

* [LangChain docs](https://langchain.com/)
* [OpenAI API concepts](https://platform.openai.com/docs)
* [Kubernetes Basics](https://kubernetes.io/docs/concepts/overview/what-is-kubernetes/)
* [Docker documentation](https://docs.docker.com/)

> **[Watch Video](https://learn.kodekloud.com/user/courses/building-kubernetes-finops-ai-agent/module/d9a1ac50-d5f4-4cc1-a8d4-379664950054/lesson/6dfccec9-9f2a-4e14-b1c1-6964340f81f1)**


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.