Skip to main content

LangChain / LangGraph Example

A real LangGraph agent running on sypho — no sypho-specific imports, just the standard LangChain/LangGraph ecosystem pointed at the platform's environment variables.

Manifest​

sypho.manifest.json
{
"schema_version": "1.0",
"agent_name": "support-agent",
"description": "LangGraph support agent with GitHub and structured-data access.",
"version": "0.1.0",
"entrypoints": {
"main": {
"description": "Looks up GitHub issues and saves notes to structured data.",
"tools": [
"mcp__github__*",
"structured_data_*"
],
"deny_tools": [
"mcp__github__delete_repo"
]
}
},
"structured_data_dependencies": {
"support-notes": {
"required": false,
"default_permission": "read_write"
}
},
"execution": {
"command": ["python", "agent.py"]
},
"secrets": {
"openai_api_key": {
"description": "OpenAI API key for LLM calls",
"required": true
}
}
}

Agent code​

agent.py
import asyncio
import os
from typing import Annotated, TypedDict
import operator

from langchain_mcp_adapters.client import MultiServerMCPClient
from langchain_openai import ChatOpenAI
from langgraph.graph import StateGraph
from langgraph.prebuilt import ToolNode, tools_condition

DEFAULT_MODEL = "anthropic/claude-haiku-4-5"
DEFAULT_SYSTEM_PROMPT = "You are a helpful assistant. Use the tools available to you when they apply."
DEFAULT_MAX_ITERATIONS = 25


class State(TypedDict):
messages: Annotated[list, operator.add]


async def main():
model = os.getenv("SYPHO_MODEL") or DEFAULT_MODEL
system_prompt = os.getenv("SYPHO_SYSTEM_PROMPT") or DEFAULT_SYSTEM_PROMPT
max_iterations = int(os.getenv("SYPHO_MAX_ITERATIONS") or DEFAULT_MAX_ITERATIONS)

# langchain-mcp-adapters discovers and binds sypho's real tools directly
# from the gateway's MCP endpoint — no sypho-specific client needed.
client = MultiServerMCPClient({
"sypho": {"url": os.environ["SYPHO_MCP_URL"], "transport": "streamable_http"},
})
tools = await client.get_tools()

# A stock OpenAI client pointed at the gateway — real auth and model
# routing happen there, so the API key here is just a placeholder.
llm = ChatOpenAI(model=model, temperature=0, api_key=os.environ["OPENAI_API_KEY"]).bind_tools(tools)

def call_llm(state: State):
return {"messages": [llm.invoke(state["messages"])]}

workflow = StateGraph(State)
workflow.add_node("llm", call_llm)
workflow.add_node("tools", ToolNode(tools))
workflow.set_entry_point("llm")
workflow.add_conditional_edges("llm", tools_condition)
workflow.add_edge("tools", "llm")

app = workflow.compile()
result = await app.ainvoke(
{"messages": [
{"role": "system", "content": system_prompt},
{"role": "user", "content": "Look up issue #42 on our repo and save a summary."},
]},
{"recursion_limit": max_iterations},
)

return {"response": result["messages"][-1].content}


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

Dockerfile​

FROM python:3.11-slim

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY agent.py .

ENTRYPOINT ["python", "agent.py"]
requirements.txt
langchain-openai
langchain-mcp-adapters
langgraph

What's actually happening​

  • MultiServerMCPClient talks to SYPHO_MCP_URL, which is the per-run gateway sidecar — every tool call it makes is checked against the manifest's tools/deny_tools patterns before it reaches the real upstream.
  • ChatOpenAI never talks to OpenAI directly. OPENAI_BASE_URL points it at the gateway, which forwards the call through the control plane's real model routing and secret resolution, using whatever model and credentials this run is actually configured with.
  • Nothing above imports anything sypho-specific. The same pattern works for CrewAI, plain LangChain, or a hand-rolled agent loop — the only requirement is reading the environment variables described in Bring Your Own Framework.

Build and deploy​

sypho init support-agent
cd support-agent
# replace the generated agent.py and requirements.txt with the files above,
# and add sypho.manifest.json

sypho build --version 0.1.0
sypho deploy --version 0.1.0
sypho run support-agent:0.1.0 --input '{"message": "go"}'