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
MultiServerMCPClienttalks toSYPHO_MCP_URL, which is the per-run gateway sidecar — every tool call it makes is checked against the manifest'stools/deny_toolspatterns before it reaches the real upstream.ChatOpenAInever talks to OpenAI directly.OPENAI_BASE_URLpoints 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"}'