MCP Mesh Decorators¶
Core decorators for building distributed agent systems
Note: This page shows Python examples. See meshctl man decorators --typescript for TypeScript or meshctl man decorators --java for Java/Spring Boot examples.
Overview¶
MCP Mesh provides decorators (Python), annotations (Java), and function wrappers (TypeScript) that transform regular functions into mesh-aware distributed services. These APIs handle registration, dependency injection, and communication automatically.
| Decorator | Purpose |
|---|---|
@mesh.agent | Configure agent server settings |
@mesh.tool | Register capability with DI |
@mesh.llm | Enable LLM-powered tools |
@mesh.llm_provider | Create LLM provider (zero-code) |
@mesh.route | FastAPI route with mesh DI |
Decorator Order (Critical!)¶
When using multiple decorators, order matters:
@app.tool() # 1. FastMCP protocol handler (outermost)
@mesh.llm(...) # 2. LLM integration (if using)
@mesh.tool(...) # 3. Mesh capability registration (innermost)
def my_function():
pass
@mesh.agent¶
Configures the agent server settings. Applied to a class.
@mesh.agent(
name="my-service", # Required: unique agent identifier
version="1.0.0", # Semantic version
description="Service desc", # Human-readable description
http_port=8080, # HTTP server port (0 = auto-assign)
http_host="localhost", # Host announced to registry
namespace="default", # Namespace for isolation
auto_run=True, # Start automatically (no main() needed)
auto_run_interval=30, # Heartbeat interval in seconds
health_check=health_fn, # Optional health check function
health_check_ttl=30, # Health check cache TTL
)
class MyAgent:
pass
auto_run: server and process lifetime, not mesh membership¶
auto_run controls who starts the HTTP server and who owns the process. It does not control whether the agent joins the mesh.
auto_run=True (default) | auto_run=False | |
|---|---|---|
| Startup pipeline runs | yes | yes |
| Registers with the registry | yes | yes |
| Heartbeats, dependencies resolve | yes | yes |
| Starts the HTTP server | yes | no — you do |
| Keeps the process alive | yes | no — your loop does |
Use auto_run=False to embed a mesh agent inside a server loop you already own. Serve on the configured http_port: that is the address the agent registers, so a mismatch registers an endpoint nothing answers on.
MCP_MESH_AUTO_RUN overrides the decorator argument, so MCP_MESH_AUTO_RUN=false behaves exactly like auto_run=False — including still registering. To make mesh inert instead, set MCP_MESH_ENABLED=false.
Startup runs on a background timer, after your decorators import and after @mesh.agent returns, so mesh can neither raise into your code nor exit the process it does not own. Poll the outcome:
import mesh
status = mesh.startup_status()
# {"state": "pending"|"ready"|"failed", "pipeline_type": ..., "error": ...,
# "heartbeat": bool, "warnings": [...]}
if status["state"] == "failed":
raise SystemExit(f"mesh failed to start: {status['error']}")
Check heartbeat and warnings even on ready: a run can succeed and still leave the agent unable to stay registered (standalone mode, or heartbeat setup failing), and those are reported rather than announced as success.
Current limits of embedded mode. Two things auto-run does for you have no public equivalent yet:
- No app accessor. The MCP pipeline builds a FastAPI app with FastMCP mounted, but it is only reachable inside the pipeline context — there is no supported way to obtain it and serve it yourself. Embedded mode is therefore practical today for
@mesh.routeand@mesh.a2aservices, where you build and own the app, and for@mesh.toolagents whose HTTP surface you do not need to serve. - No graceful deregistration. Auto-run installs a SIGTERM path that unregisters on shutdown. Embedded mode installs none, so the agent is removed by registry timeout rather than promptly on exit.
@mesh.tool¶
Registers a function as a mesh capability with dependency injection.
@app.tool()
@mesh.tool(
capability="greeting", # Capability name for discovery
description="Greets users", # Human-readable description
version="1.0.0", # Capability version
tags=["greeting", "utility"], # Tags for filtering
dependencies=["date_service"], # Required capabilities
)
async def greet(name: str, date_svc: mesh.McpMeshTool = None) -> str:
if date_svc:
today = await date_svc() # Must use await for proxy calls!
return f"Hello {name}! Today is {today}"
return f"Hello {name}!" # Graceful degradation
Note: Functions with dependencies must be async def and proxy calls require await.
Dependency Injection Types¶
| Type | Use Case |
|---|---|
mesh.McpMeshTool | Tool calls via proxy |
mesh.MeshLlmAgent | LLM agent injection (with @mesh.llm) |
@mesh.service¶
Aggregates several dotted capabilities behind a typed consumer view (RFC #1280). Decorate a class of async @mesh.selector stubs and inject it as a @mesh.tool parameter:
@mesh.service # or @mesh.service(min_available=2)
class MediaService:
@mesh.selector("media.caption", required=True)
async def caption(self, args: dict) -> dict: ...
@mesh.selector("media.thumbnail")
async def thumbnail(self, args: dict) -> dict: ...
@mesh.tool(capability="process_media")
async def process_media(req: dict, media: MediaService = None):
return await media.caption({"text": req["text"]})
The capabilities a view binds are ordinary dotted tools — each declared explicitly on its provider with @mesh.tool(capability="media.caption"):
Each view method is an ordinary dependency edge and each dotted capability an ordinary tool — both show as N entries in meshctl list. required view edges get the pre-invoke dependency_unavailable refusal; min_available (consumer-only) adds a floor. For the full semantics see the Dependency Injection guide.
@mesh.llm¶
Enables LLM-powered tools with automatic tool discovery.
@app.tool()
@mesh.llm(
provider={"capability": "llm", "tags": ["+claude"]}, # LLM provider selector
max_iterations=5, # Max agentic loop iterations
system_prompt="file://prompts/agent.jinja2", # Jinja2 template
response_model=AssistResponse, # Pydantic model the LLM must emit (optional)
context_param="ctx", # Parameter name for context
filter=[{"tags": ["tools"]}], # Tool filter for discovery
filter_mode="all", # "all", "best_match", or "*"
)
@mesh.tool(
capability="smart_assistant",
description="LLM-powered assistant",
)
async def assist(ctx: AssistContext, llm: mesh.MeshLlmAgent = None) -> AssistResponse:
return await llm("Help the user with their request")
Note: Response format is determined by return type: -> str for text, -> PydanticModel for JSON. Use response_model to make the LLM emit a focused subset (validated against that model) while the return annotation still drives the tool's outputSchema; when omitted, the LLM schema falls back to the return annotation.
Filter Modes¶
| Mode | Description |
|---|---|
all | Include all tools matching any filter |
best_match | One tool per capability (best tag match) |
* | All available tools (wildcard) |
@mesh.llm_provider¶
Creates a zero-code LLM provider wrapping LiteLLM.
@mesh.llm_provider(
model="anthropic/claude-sonnet-4-5", # LiteLLM model string
capability="llm", # Capability name
tags=["llm", "claude", "provider"], # Discovery tags
version="1.0.0", # Provider version
)
def claude_provider():
pass # No implementation needed
@mesh.route¶
Enables mesh dependency injection in FastAPI route handlers. Use this when building REST APIs that consume mesh capabilities.
from fastapi import APIRouter, Request
import mesh
from mesh.types import McpMeshTool
router = APIRouter()
@router.post("/chat")
@mesh.route(dependencies=["avatar_chat"])
async def chat_endpoint(
request: Request,
message: str,
avatar_agent: McpMeshTool = None, # Injected by mesh
):
result = await avatar_agent(message=message, user_email="user@example.com")
return {"response": result.get("message")}
Note: @mesh.route is for FastAPI backends that consume mesh capabilities. Use @mesh.tool for MCP agents that provide capabilities.
See meshctl man fastapi for complete FastAPI integration guide.
Environment Variable Overrides¶
All decorator parameters can be overridden via environment variables:
export MCP_MESH_AGENT_NAME=custom-name
export MCP_MESH_HTTP_PORT=9090
export MCP_MESH_NAMESPACE=production
export MCP_MESH_AUTO_RUN=false # No server, no blocking — still registers
See Also¶
meshctl man dependency-injection- DI detailsmeshctl man llm- LLM integration guidemeshctl man tags- Tag matching systemmeshctl man capabilities- Capabilities systemmeshctl man fastapi- FastAPI integration with @mesh.route