DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Related

  • Agents and Tools in Agentic AI: A Simple Explanation
  • How to Extract Tables from PDFs and Other Documents in C#
  • From Microservices to Agent Services: The Next Architectural Shift
  • Microsoft Foundry Tool Search: Your Agent Pays a Tax on Every Tool It Never Calls

Trending

  • Engineering Production Agentic Systems: Part 3: The Topology
  • Ampere System Profiler: A Guide to System-Level Profiling
  • Chat with Your Oracle Database: SQLcl MCP + GitHub Copilot
  • The 20 Software Engineering Laws
  1. DZone
  2. Coding
  3. Java
  4. Part 1: Building Governed MCP Tool Services With Quarkus LangChain4j and Goose

Part 1: Building Governed MCP Tool Services With Quarkus LangChain4j and Goose

Build governed, cloud-native Java MCP tool services for Goose agents using Quarkus LangChain4j, Java 25, and Jakarta Bean Validation.

By 
Daniel Oh user avatar
Daniel Oh
DZone Core CORE ·
Aug. 26, 26 · Tutorial
Likes (1)
Comment
Save
Tweet
Share
99 Views

Join the DZone community and get the full member experience.

Join For Free

Goose — the open-source, Rust-based AI developer agent from Block (donated to the Linux Foundation’s Agentic AI Foundation) — interacts natively with your local development environment via the Model Context Protocol (MCP). In this tutorial, you will learn how to build stateless, cloud-native Java microservices using Quarkus LangChain4j and expose them as governed MCP extensions that Goose can discover and run seamlessly.

Autonomous AI coding agents like Goose go far beyond simple code autocompletion. Built in Rust for speed and portability, Goose runs on your local machine, inspects files, runs terminal commands, and uses tools over MCP to automate complex engineering tasks.

However, when developers want an AI agent to query enterprise microservices, trigger database migrations, or fetch internal API metrics, writing custom local scripts or ad-hoc wrappers is brittle and dangerous.

The solution is to build a stateless MCP Tool Server in Java using Quarkus LangChain4j. Quarkus provides near-zero startup time and low memory footprint, while LangChain4j makes exposing @Tool methods via standard MCP HTTP/JSON-RPC trivial.

Architecture: How Goose Integrates With Quarkus MCP

Markdown
 
┌────────────────────────────────────────────────────────┐
│             Goose AI Agent (Rust Runtime)              │
│       (Local CLI / Desktop App / ACP Server)           │
└───────────────────────────┬────────────────────────────┘
                            │ Model Context Protocol (MCP)
                            │ JSON-RPC over Stateless HTTP
                            ▼
┌────────────────────────────────────────────────────────┐
│            Quarkus LangChain4j MCP Server              │
│     - @Tool Annotations & Bean Validation              │
│     - Reactive SmallRye Mutiny Execution               │
│     - GraalVM Native Image Ready                       │
└───────────────────────────┬────────────────────────────┘
                            │ Reactive Clients
                            ▼
             Enterprise APIs / Databases / Dev UI


  1. Goose Agent (Client): Executes on the developer machine, orchestrating LLM tool loops via MCP.
  2. MCP HTTP Transport: Goose sends structured tool calls to the Quarkus backend as stateless HTTP POST requests using standardized MCP methods (tools/list, tools/call).
  3. Quarkus Microservice: Validates parameters with Jakarta Bean Validation, executes reactive business logic, and returns structured data to Goose.

Step 1: Configuring Dependencies in Quarkus

Create a new Quarkus project or update your pom.xml to include quarkus-langchain4j-mcp and Reactive:

Note: Find the completed demo application here: https://github.com/danieloh30/governed-mcp-tools.git.

XML
 
<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>
    </dependencies>
</dependencyManagement>

<dependencies>
    <dependency>
      <groupId>io.quarkus</groupId>
      <artifactId>quarkus-arc</artifactId>
    </dependency>
    <dependency>
      <groupId>io.quarkus</groupId>
      <artifactId>quarkus-rest-jackson</artifactId>
    </dependency>
    <dependency>
      <groupId>io.quarkiverse.mcp</groupId>
      <artifactId>quarkus-mcp-server-http</artifactId>
      <version>2.0.0.CR2</version>
    </dependency>
    <dependency>
      <groupId>io.quarkus</groupId>
      <artifactId>quarkus-hibernate-validator</artifactId>
    </dependency>
    <dependency>
      <groupId>io.quarkus</groupId>
      <artifactId>quarkus-junit</artifactId>
      <scope>test</scope>
    </dependency>
  </dependencies>


Step 2: Implementing Hardened MCP Tools

We will create a Customer Services MCP Tool that Goose can call when an engineer asks: "Goose, check the database status for customer CUST-4091 and fetch their recent telemetry."

By placing @Tool annotations on CDI beans, Quarkus LangChain4j automatically registers the class as an MCP server endpoint:

Embedded Javascript
 
@ApplicationScoped
public class CustomerServiceTools {

    @Tool(description = "Retrieve the current account status, service tier, and primary deployment region for a given customer.")
    public Uni<CustomerStatusResponse> getCustomerStatus(
            @ToolArg(description = "Customer ID formatted as CUST-XXXX")
            @NotNull
            @Pattern(regexp = "^CUST-[0-9]{4,8}$")
            String customerId) {
        CustomerStatusResponse response = switch (customerId) {
            case "CUST-4091" -> new CustomerStatusResponse("CUST-4091", "ACTIVE", "ENTERPRISE_TIER", "US-EAST-1");
            case "CUST-2187" -> new CustomerStatusResponse("CUST-2187", "ACTIVE", "BUSINESS_TIER", "EU-WEST-1");
            case "CUST-7734" -> new CustomerStatusResponse("CUST-7734", "SUSPENDED", "STARTER_TIER", "AP-SOUTH-1");
            default -> new CustomerStatusResponse(customerId, "NOT_FOUND", "UNKNOWN", "UNKNOWN");
        };
        return Uni.createFrom().item(response);
    }

    @Tool(description = "Retrieve recent health-check logs and diagnostic metrics for a specified availability zone.")
    public Uni<List<String>> getZoneHealthLogs(
            @ToolArg(description = "Zone identifier, e.g., US-EAST-1")
            @Size(max = 20)
            String zoneId) {
        return Uni.createFrom().item(List.of(
                "[" + zoneId + "] CPU utilization: 42% (healthy)",
                "[" + zoneId + "] Memory pressure: 31% (normal)",
                "[" + zoneId + "] Network I/O: 1.2 Gbps ingress / 0.8 Gbps egress",
                "[" + zoneId + "] Disk IOPS: 12,400 read / 8,300 write (within SLA)",
                "[" + zoneId + "] Active connections: 18,230 (capacity: 50,000)",
                "[" + zoneId + "] Last incident: none in past 72 hours"
        ));
    }

    @Tool(description = "Track the current status, item count, and estimated delivery for an enterprise order.")
    public Uni<OrderStatusResponse> getOrderStatus(
            @ToolArg(description = "Order ID formatted as ORD-XXXXXXXX")
            @NotNull
            @Pattern(regexp = "^ORD-[0-9]{8}$")
            String orderId) {
        OrderStatusResponse response = switch (orderId) {
            case "ORD-20240815" -> new OrderStatusResponse("ORD-20240815", "SHIPPED", 12, "$48,750.00", "2024-08-22", "US-EAST-1");
            case "ORD-20240901" -> new OrderStatusResponse("ORD-20240901", "PROCESSING", 5, "$12,300.00", "2024-09-10", "EU-WEST-1");
            case "ORD-20241003" -> new OrderStatusResponse("ORD-20241003", "DELIVERED", 28, "$134,500.00", "2024-10-08", "AP-SOUTH-1");
            default -> new OrderStatusResponse(orderId, "NOT_FOUND", 0, "$0.00", "N/A", "UNKNOWN");
        };
        return Uni.createFrom().item(response);
    }

    @Tool(description = "Retrieve SLA compliance metrics including uptime, latency, and violation count for a service.")
    public Uni<SLAComplianceResponse> getSLACompliance(
            @ToolArg(description = "Service identifier, e.g., api-gateway, auth-service")
            @NotNull
            @Size(max = 40)
            String serviceId) {
        SLAComplianceResponse response = switch (serviceId) {
            case "api-gateway" -> new SLAComplianceResponse("api-gateway", 99.97, "45ms", 99.99, 0, "2024-Q3");
            case "auth-service" -> new SLAComplianceResponse("auth-service", 99.82, "120ms", 99.95, 3, "2024-Q3");
            case "data-pipeline" -> new SLAComplianceResponse("data-pipeline", 98.50, "340ms", 99.80, 12, "2024-Q3");
            case "notification-hub" -> new SLAComplianceResponse("notification-hub", 99.91, "78ms", 99.97, 1, "2024-Q3");
            default -> new SLAComplianceResponse(serviceId, 0.0, "N/A", 0.0, -1, "N/A");
        };
        return Uni.createFrom().item(response);
    }
...
}


Step 3: Enabling the MCP Extension in application.properties

Configure your Quarkus MCP server settings:

Properties files
 
quarkus.mcp-server.server-info.name=customer-tools
quarkus.mcp-server.server-info.version=1.0.0
quarkus.mcp-server.http.root-path=/mcp

quarkus.log.category."io.quarkiverse.mcp".level=DEBUG


Launch Quarkus in dev mode:

Shell
 
./mvnw quarkus:dev


Step 4: Connecting Goose to Your Quarkus MCP Server

Goose can be extended with any MCP server over stdio or HTTP. Configure Goose by editing its YAML configuration file or using the Goose CLI.

Option A: Using the Goose CLI

Register the Quarkus MCP server directly in your terminal:

Shell
 
goose extension add customer-tools \
  --type http \
  --uri http://localhost:8080/mcp


Option B: Editing ~/.config/goose/config.yaml

Add the Quarkus backend to your Goose extensions configuration:

YAML
 
extensions:
  customer-tools:
    enabled: true
    type: http
    uri: http://localhost:8080/mcp
    headers:
      Content-Type: "application/json"


Step 5: Testing the Developer Workflow

Launch Goose via CLI or the Desktop App:

Shell
 
goose session


Prompt Goose:

Developer: "I'm debugging customer CUST-4091. Use customer-tools to fetch their account tier, and then check the health logs for their primary region."

Frontend UI:

Developer: Choose one of the Tool explorers. Select the “Run tool” button on the right panel. Verify the audit events.

Frontend UI

What Happens Under the Hood

  1. Discovery: Goose sends an HTTP POST /mcp JSON-RPC tools/list request. Quarkus responds with JSON schema definitions derived from getCustomerStatus and getZoneHealthLogs.
  2. Tool Invocation 1: Goose parses the prompt, formats a tools/call JSON payload with {"customerId": "CUST-4091"}, and posts it to Quarkus.
  3. Execution and validation: Quarkus executes Hibernate Bean Validation. Since CUST-4091 matches ^CUST-[0-9]{4,8}$, it runs getCustomerStatus and returns primaryRegion: US-EAST-1.
  4. Tool Invocation 2: Goose sees US-EAST-1, triggers getZoneHealthLogs("US-EAST-1"), receives the green health metrics, and summarizes the complete diagnostic report back to you in the CLI.

Summary and Next Steps

By wrapping Java business logic in Quarkus LangChain4j @Tool beans, you give local AI developer agents like Goose secure, validated access to enterprise backend systems.

However, when hundreds of developers run local Goose agents against shared backend microservices in production, connecting them directly creates security and governance risks.

Coming up in Part 2: We will introduce agentgateway — the Linux Foundation data plane proxy —to sit between Goose and Quarkus. We will configure OAuth2/OIDC authentication, fine-grained tool-level RBAC, and rate limiting to harden our enterprise AI infrastructure.

Quarkus Tool

Opinions expressed by DZone contributors are their own.

Related

  • Agents and Tools in Agentic AI: A Simple Explanation
  • How to Extract Tables from PDFs and Other Documents in C#
  • From Microservices to Agent Services: The Next Architectural Shift
  • Microsoft Foundry Tool Search: Your Agent Pays a Tax on Every Tool It Never Calls

Partner Resources

×

Comments

The likes didn't load as expected. Please refresh the page and try again.

  • RSS
  • X
  • Facebook

ABOUT US

  • About DZone
  • Support and feedback
  • Community research

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 215
  • Nashville, TN 37211
  • [email protected]

Let's be friends:

  • RSS
  • X
  • Facebook