Orchestration Meets MCP: Building Governed Agentic Workflows With Quarkus Flow and AGENTS.md
By combining Quarkus Flow, LangChain4j, MCP tools, and AGENTS.md, developers can construct deterministic, tool-augmented, and enterprise-governed AI agent loops.
Join the DZone community and get the full member experience.
Join For FreeBuilding autonomous AI agents with large language models (LLMs) is easy when writing single-turn demo scripts. However, moving multi-agent loops into production introduces serious architectural challenges. Agents hallucinate, loop infinitely without reaching convergence, require human approval for high-risk operations, and need standard tool-calling integrations alongside clear operational governance.
Historically, Java developers faced a tough choice: either rely on heavyweight, external workflow clusters (like Temporal or Camunda) that add operational overhead, or hand-craft fragile while loops and custom state machines inside their services.
Quarkus Flow bridges this gap. Built on the Cloud Native Computing Foundation (CNCF) Serverless Workflow specification, Quarkus Flow brings light-footprint, specification-compliant workflow orchestration directly into your Quarkus application. When combined with LangChain4j, Model Context Protocol (MCP) tool connections, and AGENTS.md context governance, Java developers can construct deterministic, observable, and resilient agentic AI workflows using idiomatic CDI and a fluent Java DSL.
The Modern Agentic Stack: Quarkus Flow, MCP, and AGENTS.md
To run production AI agents, you need three distinct layers: orchestration, standardized tool connectivity, and behavioral governance.
- Orchestration (Quarkus Flow): Manages state transitions, retries, conditional loops, max-iteration caps, and Human-in-the-Loop (HITL) gates inside the JVM.
- Tool standardization (MCP): Connects agents to enterprise data, databases, and APIs using the Model Context Protocol (MCP) without writing custom API adapters for every LLM host.
- Behavioral governance (AGENTS.md): A project-level markdown specification that defines system boundaries, agent roles, required output formats, and safety rules that agents read at runtime.
┌────────────────────────────────────────────────────────────────────────┐
│ `AGENTS.md` Governance │
│ (Runtime System Prompts, Rules & Security Boundaries) │
└───────────────────────────────────┬────────────────────────────────────┘
│ Loaded via GovernanceLoader
▼
┌────────────────────────────────────────────────────────────────────────┐
│ ArticlePublisherWorkflow (Quarkus Flow) │
│ │
│ 1. generateDraft ──> 2. evaluateDraft ──> 3. reviewCheck │
│ (Writer) (Critic) │ │
│ ▲ │ [approved || >=3] │
│ │ ├───> 5. publishArticle │
│ │ 4. reviseDraft <────────────┤ │
│ └─────────────────┘ [needs revision] │ │
└──────────────────────────┬─────────────────────────────────────────────┘
│
│ Tool Invocation via McpToolProvider
▼
┌────────────────────────────────────────────────────────────────────────┐
│ Stateless MCP Servers │
│ (External Data, Database Tools, & APIs) │
└────────────────────────────────────────────────────────────────────────┘
Defining Governance With AGENTS.md
Instead of hardcoding prompt strings deep inside Java classes, place an AGENTS.md file in your src/main/resources. This allows developers and prompt engineers to adjust system instructions and security boundaries without re-compiling the application.
Here is the src/main/resources/AGENTS.md file based on the reference repository:
# Content Reviewer Agent Governance & Rules
## Writer Agent Rules
- You are an expert Java and Quarkus developer.
- Draft concise, technically accurate blog posts based on requested topics.
- Query available MCP tools when database context or tool parameters are required.
## Critic Agent Rules
- You are a strict editor reviewing for clarity, security, and technical accuracy.
- Return ONLY a valid JSON object matching this schema:
{"approved": boolean, "feedback": "string"}
## Security Boundaries
- Do not output shell commands or execute arbitrary code.
- Always enforce character limits and avoid hallucinated imports.
Practical Example: Multi-Agent Workflow With MCP and AGENTS.md
Let's build a production-grade Content Publisher Agent Workflow matching the exact structure from quarkus-flow-mcp-agents. The workflow reads system instructions from AGENTS.md, uses a Writer Agent that fetches real data via an MCP Server, submits the draft to a Critic Agent, and loops until approved or max iterations are reached.
Note: You can find the complete reference implementation repository at https://github.com/danieloh30/quarkus-flow-mcp-agents.git.
1. pom.xml Dependencies
...
<properties>
<compiler-plugin.version>3.15.0</compiler-plugin.version>
<maven.compiler.release>25</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<quarkus.platform.artifact-id>quarkus-bom</quarkus.platform.artifact-id>
<quarkus.platform.group-id>io.quarkus.platform</quarkus.platform.group-id>
<quarkus.platform.version>3.38.0</quarkus.platform.version>
<skipITs>true</skipITs>
<surefire-plugin.version>3.5.6</surefire-plugin.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>${quarkus.platform.group-id}</groupId>
<artifactId>${quarkus.platform.artifact-id}</artifactId>
<version>${quarkus.platform.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>${quarkus.platform.group-id}</groupId>
<artifactId>quarkus-langchain4j-bom</artifactId>
<version>${quarkus.platform.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>${quarkus.platform.group-id}</groupId>
<artifactId>quarkus-flow-bom</artifactId>
<version>${quarkus.platform.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
...
<dependency>
<groupId>io.quarkiverse.langchain4j</groupId>
<artifactId>quarkus-langchain4j-openai</artifactId>
</dependency>
<dependency>
<groupId>io.quarkiverse.langchain4j</groupId>
<artifactId>quarkus-langchain4j-mcp</artifactId>
</dependency>
<dependency>
<groupId>io.quarkiverse.flow</groupId>
<artifactId>quarkus-flow-langchain4j</artifactId>
</dependency>
...
</dependencies>
...
2. Application Configuration: src/main/resources/application.properties
# Enable OpenAI
quarkus.langchain4j.openai.api-key=${OPENAI_API_KEY}
quarkus.langchain4j.openai.chat-model.model-name=gpt-4o-mini
quarkus.langchain4j.openai.log-requests=true
quarkus.langchain4j.openai.log-responses=true
3. Orchestrating the Write-Review Loop With @LoopAgent
ArticlePublisher is the orchestrator that wires the multi-agent loop together using Quarkus Flow's declarative API. Here's what each annotation does:
- @LoopAgent – runs WriterAgent then CriticAgent repeatedly (up to 3 iterations). At build time, Quarkus Flow compiles this into a CNCF Serverless Workflow definition — no separate workflow engine at runtime.
- @ExitCondition – a static method (isApproved) that checks if the critic's review starts with "APPROVED". It runs after each loop iteration (testExitAtLoopEnd = true). If true, the loop breaks early.
- @Output – a static method (extractArticle) that extracts the final result. It pulls the draft from the shared agent scope and returns it as the workflow output.
- The flow: Writer drafts → Critic reviews → if not approved, Writer revises using feedback → repeat until approved or 3 iterations hit → return the final draft.
public interface ArticlePublisher {
@LoopAgent(
subAgents = { WriterAgent.class, CriticAgent.class },
maxIterations = 3)
String publishArticle(String topic);
@ExitCondition(testExitAtLoopEnd = true,
description = "Exit when the critic approves the draft")
static boolean isApproved(String review) {
return review != null && review.toUpperCase().startsWith("APPROVED");
}
@Output
static String extractArticle(String draft) {
return draft;
}
}
4. WriterAgent — Drafting With MCP-Powered Research
WriterAgent is a declarative LLM agent that researches a topic via Brave Search and drafts a technical blog post.
- @Agent – marks the method as an agent entry point. outputKey = "draft" stores the result in the shared scope so other agents (like CriticAgent) can access it.
- @ToolBox(WebSearchTool.class) – gives the LLM access to the webSearch tool. The LLM decides when to call it based on the prompt — it's not forced. This is how MCP tools connect to declarative agents.
- @SystemMessage – instructs the LLM to research before writing, produce accurate content, and revise based on prior feedback. That last part is critical for the loop — on iteration 2+, the LLM sees the critic's feedback in the chat memory and adjusts the draft accordingly.
The interface has no implementation — Quarkus generates it at build time.
public interface WriterAgent {
@Agent(outputKey = "draft",
description = "Drafts or revises a technical article based on the topic")
@ToolBox(WebSearchTool.class)
@SystemMessage("""
You are an expert Java and Quarkus developer.
Use the webSearch tool to research the topic before writing.
Write concise, technically accurate blog drafts based on your research.
Never generate raw shell commands or suggest unsafe practices.
If the reviewer has given you feedback in a previous turn, revise the draft to address it.
""")
@UserMessage("Write a short technical blog post about: {topic}")
String writeDraft(String topic);
}
5. CriticAgent — Reviewing for Accuracy and Clarity
CriticAgent is the quality gate in the loop. It reviews the draft and either approves or rejects it with feedback.
- @Agent – outputKey = "review" stores the review in the shared scope. The @ExitCondition in ArticlePublisher reads this key to decide whether to exit the loop.
- @UserMessage – injects the {draft} variable from the shared scope, so the critic always reviews the latest version of the article.
- @SystemMessage – enforces a strict contract: if the draft is acceptable, the response must start with "APPROVED:". This is what makes the @ExitCondition work — it's a simple string check, not another LLM call.
No tools are attached — the critic relies solely on the LLM's reasoning to evaluate the draft.
public interface CriticAgent {
@Agent(outputKey = "review",
description = "Reviews the draft for technical accuracy and clarity")
@SystemMessage("""
You are a strict editor checking for technical accuracy and clarity.
If the draft is acceptable, your response MUST start with "APPROVED:" followed by a brief note.
If the draft needs improvement, provide constructive feedback.
""")
@UserMessage("""
Review this draft:
{draft}
""")
String reviewDraft(String draft);
}
5. WebSearchTool — Bridging MCP and Declarative Agents
WebSearchTool is a CDI bean that connects the Brave Search MCP server to the agent workflow.
- Why it exists – @McpToolBox only works with @RegisterAiService, not with @Agent. This class bridges that gap by creating an MCP client programmatically and exposing it as a @Tool.
- MCP client setup – the constructor creates a DefaultMcpClient with stdio transport, spawning npx -y @brave/brave-search-mcp-server as a subprocess. The BRAVE_API_KEY is passed via environment variables.
- @Tool – the webSearch method builds a ToolExecutionRequest targeting the brave_web_search tool on the MCP server, executes it, and returns the results. The LLM sees this as a regular function it can call.
- @PreDestroy – cleans up the MCP client (and the subprocess) when the CDI context shuts down.
This pattern — wrapping an MCP client in a @Tool CDI bean and attaching it via @ToolBox — is reusable for any MCP server you want to connect to a declarative @Agent.
public class WebSearchTool {
private final McpClient mcpClient;
WebSearchTool(@ConfigProperty(name = "brave.api.key", defaultValue = "${BRAVE_API_KEY:}") String braveApiKey) {
mcpClient = new DefaultMcpClient.Builder()
.transport(new StdioMcpTransport.Builder()
.command(List.of("npx", "-y", "@brave/brave-search-mcp-server"))
.environment(Map.of("BRAVE_API_KEY", braveApiKey))
.logEvents(true)
.build())
.build();
}
@Tool("Search the web for up-to-date information about a given query using Brave Search")
public String webSearch(String query) {
var request = ToolExecutionRequest.builder()
.name("brave_web_search")
.arguments("{\"query\": \"" + query + "\"}")
.build();
return mcpClient.executeTool(request).resultText();
}
@PreDestroy
void close() {
try {
mcpClient.close();
} catch (Exception ignored) {
}
}
}
Production Guardrails and Enterprise Readiness
Deploying agentic AI systems into enterprise cloud environments requires strict governance, tracing, and high performance:
- Standardized tools via MCP: By consuming external systems through stateless Model Context Protocol endpoints, tool definitions are decoupled from LLM host code.
- Context control with AGENTS.md: Business analysts and security leads can audit or update prompt guidelines without re-deploying code artifacts.
- Human-in-the-loop (HITL): Use Quarkus Flow event filters or pause states to suspend execution until a human administrator approves sensitive tool actions.
- OpenTelemetry and distributed tracing: Quarkus Flow and quarkus-opentelemetry pass W3C trace contexts across every workflow transition, LLM call, and MCP request.
- GraalVM native images: Compile the entire stack — Quarkus Flow engine, LangChain4j, MCP connections, and REST interface — into an ultra-fast, native binary with sub-10ms startup times and minimal memory footprint.
By combining Quarkus Flow, LangChain4j, MCP, and AGENTS.md, Java developers can replace unmaintainable AI scripts with clean, specification-compliant, and enterprise-ready agentic architectures.
Opinions expressed by DZone contributors are their own.
Comments