Distributed Service Mesh for AI Agents¶
You write the logic. The mesh discovers, connects, heals, and traces — across languages, machines, and clouds.
Complete Platform for AI Agents
MCP Mesh is a complete platform for building and deploying AI agents to production scale — and it's all built on one idea, DDDI: you declare a capability, and the mesh resolves, types, heals, and hot-swaps it at runtime. See how MCP Mesh compares →
What is DDDI?
Distributed Dynamic Dependency Injection — dependencies are discovered, injected, and updated at runtime across machines, languages, and clouds. No configuration files, no restart required. Learn more →
Quick Overview¶
The same agent, in each language.
from fastmcp import FastMCP
import mesh
app = FastMCP("TripPlanner")
@app.tool()
@mesh.tool(
capability="plan_trip",
dependencies=[
{"capability": "weather", "tags": ["+claude"]},
{"capability": "hotels", "tags": ["+gpt"]},
{"capability": "flights"},
{"capability": "budget", "tags": ["+claude"]},
],
)
async def plan_trip(
destination: str,
dates: str,
weather: mesh.McpMeshTool = None,
hotels: mesh.McpMeshTool = None,
flights: mesh.McpMeshTool = None,
budget: mesh.McpMeshTool = None,
) -> TripPlan:
forecast = await weather(destination=destination, dates=dates)
options = await hotels(destination=destination, dates=dates)
routes = await flights(destination=destination, dates=dates)
cost = await budget(routes=routes, options=options)
return TripPlan(forecast, options, routes, cost)
@mesh.agent(name="trip-planner", auto_run=True)
class TripAgent: pass
<dependency>
<groupId>io.mcp-mesh</groupId>
<artifactId>mcp-mesh-spring-boot-starter</artifactId>
<version>3.7.1</version>
</dependency>
import io.mcpmesh.MeshAgent;
import io.mcpmesh.MeshTool;
import io.mcpmesh.Param;
import io.mcpmesh.Selector;
import io.mcpmesh.types.McpMeshTool;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import java.util.Map;
@MeshAgent(name = "trip-planner", version = "1.0.0", port = 8080)
@SpringBootApplication
public class TripPlannerApp {
public static void main(String[] args) {
SpringApplication.run(TripPlannerApp.class, args);
}
@MeshTool(
capability = "plan_trip",
dependencies = {
@Selector(capability = "weather", tags = {"+claude"}),
@Selector(capability = "hotels", tags = {"+gpt"}),
@Selector(capability = "flights"),
@Selector(capability = "budget", tags = {"+claude"})
}
)
public TripPlan planTrip(
@Param("destination") String destination,
@Param("dates") String dates,
McpMeshTool<Forecast> weather,
McpMeshTool<HotelOptions> hotels,
McpMeshTool<FlightRoutes> flights,
McpMeshTool<Cost> budget
) {
var args = Map.of("destination", destination, "dates", dates);
var forecast = weather.call(args);
var options = hotels.call(args);
var routes = flights.call(args);
var cost = budget.call(Map.of("routes", routes, "options", options));
return new TripPlan(forecast, options, routes, cost);
}
}
import { FastMCP, mesh, McpMeshTool } from "@mcpmesh/sdk";
import { z } from "zod";
const server = new FastMCP({ name: "TripPlanner", version: "1.0.0" });
const agent = mesh(server, { name: "trip-planner", httpPort: 8080 });
agent.addTool({
name: "plan_trip",
capability: "plan_trip",
description: "Plan a trip by composing weather, hotels, flights, and budget",
dependencies: [
{ capability: "weather", tags: ["+claude"] },
{ capability: "hotels", tags: ["+gpt"] },
{ capability: "flights" },
{ capability: "budget", tags: ["+claude"] },
],
parameters: z.object({
destination: z.string(),
dates: z.string(),
}),
execute: async (
{ destination, dates },
weather: McpMeshTool | null,
hotels: McpMeshTool | null,
flights: McpMeshTool | null,
budget: McpMeshTool | null,
) => {
const forecast = await weather!({ destination, dates });
const options = await hotels!({ destination, dates });
const routes = await flights!({ destination, dates });
const cost = await budget!({ routes, options });
return { forecast, options, routes, cost };
},
});
What just happened?
Four distributed calls, composed like a local function. Each dep could live in this process, another machine, another language. Mesh handles discovery, transport, retry, and failover — your function stays a function. Each dep is just another @mesh.tool, defined the same way — in this agent or another.
Any dep can be a plain tool or an LLM agent — your code can't tell. weather could be a REST API or a Claude-powered reasoning agent returning a typed pydantic forecast. +claude means prefer the reasoning agent; if it dies, mesh auto-rewires to the API. When Claude recovers, mesh rewires back. No deploy, no config, no code change.
See how the Claude-powered weather agent is built (10 lines)
from fastmcp import FastMCP
import mesh
app = FastMCP("ClaudeWeather")
@app.tool()
@mesh.llm(
system_prompt="file://prompts/weather.j2",
provider={"capability": "llm", "tags": ["+claude"]},
)
@mesh.tool(capability="weather", tags=["+claude"])
async def weather(destination: str, dates: str,
llm: mesh.MeshLlmAgent = None) -> Forecast:
return await llm(f"Forecast for {destination} on {dates}")
@mesh.agent(name="claude-weather", auto_run=True)
class Agent: pass
The LLM orchestrates tools via the mesh filter pattern and returns a typed pydantic Forecast — no agentic loop to write.
Route by Python if/else, not config
See the full TripPlanner tutorial →
Getting Started¶
Start with the CLI — fastest way to explore mesh, scaffold agents, and read documentation offline.
meshctl is a fully-featured command-line tool that follows you from your first agent through production and beyond: scaffolding, local dev, registry inspection, tracing, observability, deployment, and operations are all one command away. Explore the full CLI reference →
# Install the CLI
npm install -g @mcpmesh/cli
# Explore commands
meshctl --help
# Built-in documentation
meshctl man
Turn your AI coding assistant into a mesh expert
Working with Claude Code, Cursor, Copilot, or any other AI coding assistant? Ask it to run meshctl man and read through the topics it surfaces. The built-in man pages cover every feature in depth — within a few minutes your assistant will be fluent in mesh, ready to scaffold agents, debug DDDI wiring, and answer architecture questions without you having to copy-paste docs into the chat.
01 ARRIVE
It starts with one.
@mesh.tool(capability="flight_search")
This is the mesh dashboard. Every card is a running agent; every line is a dependency the mesh resolved on its own. Right now there is one agent — a plain Python function that searches flights. No server code, no registration call, no config file.
01 ARRIVE
It needs what it doesn't have.
dependencies=["user_preferences"]
The flight agent needs preferences it cannot provide itself. It names the capability — not a host, not a port, not a URL — and the mesh finds whoever offers it and injects it as a callable parameter. Four more agents come up, and two relationships form without either side being told where the other lives.
02 THINK
Now it needs to reason.
@mesh.llm(provider={"capability": "llm"})
Some problems don't decompose into function calls. The planner needs a model, so it asks for one the same way anything else asks for anything — by capability. It imports no vendor SDK and reads no API key. A provider agent advertises llm, and that is the whole integration.
02 THINK
Reasoning picks its own collaborators.
filter=[{"capability": "flight_search"}, …]
Instead of a fixed call graph, the planner declares what kind of help the model may recruit. The mesh resolves everything matching and hands them over as callable tools. Add a new agent to the mesh tomorrow and the model can reach it — with no redeploy and no change to this code.
02 THINK
And the ones that asked are now asked.
capability="flight_search" — consumed, and consuming
Every consumer is also a provider. The flight agent that needed preferences a moment ago is now what the planner is looking for; the same card carries an edge in each direction. No one brokered the introduction and nothing was registered twice. This is the whole idea — not a call graph with a root, but a set of mutual needs that happen to resolve.
03 SURVIVE
A better one arrives.
provider={"capability":"llm","tags":["+claude"]}
A second provider joins, offering the same llm capability. The planner never named an endpoint, so nothing has to be rewired to consider it — it expresses a preference with a tag and the registry scores the candidates. Both remain eligible. One simply wins.
03 SURVIVE
It dies.
meshctl stop claude-provider
The preferred provider stops. Its heartbeat lapses, the registry ages it out, and the planner's dependency count drops. Nothing crashed and nothing was alerted. The mesh has simply stopped counting on something that is no longer there.
03 SURVIVE
Life goes on.
no deploy · no config · no code change
The planner's requirement was a capability, not an address — and something else already satisfies it. Traffic moves. Exactly one edge on this screen changed; every other relationship is untouched, and no agent was restarted to make it happen.
03 SURVIVE
The old one returns.
+claude scores higher — both are ready
It comes back, and traffic returns to it. Not because the substitute failed — it stayed healthy and connected the entire time — but because preference is scored continuously, not decided once at startup. Relationships here are never permanent, in either direction.
04 OPEN
The outside world wants in.
@mesh.route(dependencies=["trip_planning"])
A five-line HTTP handler inherits the entire mesh. It provides no capability of its own and contains no business logic — it declares what it needs and becomes a front door. The same agents are also reachable over MCP and A2A without changing a line.
04 OPEN
One need, many minds.
-> BudgetAnalysis — typed, validated, retried
Three specialists resolve as ordinary callables and run at once. Each returns a typed model rather than a blob of text, and the mesh retries the call if a response doesn't match the schema. Fan-out costs one line, because parallelism was never the hard part — knowing who to call was.
05 GROW
Rewritten. Nobody noticed.
weather-agent -> TypeScript
· hotel-agent -> Java
Two agents were replaced with implementations in different languages. Same capabilities, same names, same relationships. Look at the graph: nothing moved. Their dependents were never told, because a dependent asks for a capability and has no way to express a preference about the language behind it.
05 GROW
More of it. Same relationships.
replicaCount: 3
Three instances register under one name and collapse into a single card. Nothing re-resolves and no consumer is reconfigured — behind a Kubernetes Service the mesh resolves the name once and Kubernetes spreads the calls across whoever is healthy.
05 GROW
Nothing here was wired by hand.
twelve agents · three languages
· every edge resolved itself
Infrastructure that expects agents.
MCP · A2A · REST — three protocols, three languages, three vendors, one decorator
LEARN
The man pages are compiled into the binary, so they describe the version you actually have rather than whatever shipped last. Seventeen topics answer in Python, TypeScript or Java. A --raw mode turns your AI coding assistant into a mesh expert, and a ten-day tutorial ships inside the CLI.
DEVELOP
Python, TypeScript, and Java agents discover and call each other through a shared Rust core. One scaffold command emits the agent, its Dockerfile, and its Helm values. Claude, GPT, and Gemini are native; a hundred more arrive through LiteLLM, the Vercel AI SDK, or Spring AI.
TEST
Nothing points at a URL, so nothing needs repointing to test. Run an ordinary agent as a stand-in on your laptop — no mock framework, no special annotation — and the local registry wires your consumer to it. What differs between laptop and production is who registered, never your code.
DEPLOY
The same agent code runs on a laptop, in Docker Compose, and on Kubernetes with no changes. Nothing in it names where anything lives, so there is nothing to repoint when it moves. The health probes Kubernetes wants are already served, and scaling is one value in a file.
SECURE
Every inter-agent call is mutually authenticated. Identity is checked with X.509 before a registration is accepted, backed by files, HashiCorp Vault PKI, or SPIRE workload identity. Certificates rotate through the heartbeat without a restart — and on Linux, private keys live in tmpfs and never touch disk.
OBSERVE
Spans cross language boundaries into one trace tree: a Python call into Java into TypeScript reads as a single trace. Redis carries the stream, Tempo stores it, and three Grafana dashboards come prebuilt. meshctl trace renders the call tree in your terminal.
Build one yourself.
npm install -g @mcpmesh/cli · meshctl scaffold
Ready to get started?
Python SDK Java SDK TypeScript SDK View on GitHub
Star the repo if MCP Mesh helps you build better AI systems!