Stop Letting AI Guess in Your Java Repository
Build a grounded coding environment with JDTLS, filesystem scope, Git context, and conventions so your assistant follows the code you actually ship
A ticket says “add CSV export for cargo itineraries.” That sounds small until an AI coding tool starts working on a layered Jakarta EE codebase without a map. Now the real problem shows up. Which service already owns itinerary lookup? Which REST facade should expose the export? Which exception already means “not found”? Which serialization path is allowed in this repository? Without those answers, the tool does what tools do. It guesses, and the guesses look convincing right up to the point where they collide with the code you actually ship.
The problem is usually earlier than the prompt. In a layered Jakarta EE application, the hard part is not understanding English. The hard part is knowing where the system already solves the problem. A request like “add CSV export for cargo itineraries” sounds simple. In a real repository, it is a navigation problem. Which service already owns itinerary lookup? Which facade already exposes cargo data? Which exception means not found? Which serialization stack is allowed? Which package is off-limits because it breaks layering?
Senior developers do this mapping almost automatically. They look at the project structure, the recent commit history, the conventions, and the existing call graph before they write a line of code. AI coding tools fail when we skip that step and ask them to implement immediately. Then the tool guesses. It invents a helper class in the wrong layer. It creates a second copy of logic that already exists. It adds an endpoint that looks right but does not fit the API surface you actually ship.
That failure is easy to misread. It looks like “AI cannot do real work.” But the real issue is environmental. You asked the assistant to act like a senior teammate without giving it the same working context a senior teammate would demand on day one. No language server. No file boundaries. No recent history. No written rules. No project-local configuration that travels with the repository.
In this tutorial, we fix that problem the practical way. We build a small harness around the Jakarta EE Cargo Tracker example so IBM Bob can reason against the actual repository instead of guessing from text alone. We use the Eclipse JDT Language Server behind an MCP bridge, a filesystem server scoped to source code, optional Git context, a committed .bob/mcp.json, and two human checkpoints before implementation starts. Some people call this harness engineering. Some call it context engineering. The exact name is still settling. The important part is simple: structure in, structure out.
By the end, you will have a repeatable setup you can reuse for Cargo Tracker and adapt to your own Jakarta EE or Spring repositories.
Prerequisites
You do not need deep AI tooling knowledge for this tutorial, but you should be comfortable on the command line and able to read a Maven-based Java project. We will use IBM Bob in the examples, but the same idea works with any MCP-capable client that can launch external servers and consume project-local configuration.
Java 21 or later installed
Maven Wrapper available in the target repository
Maven 3.9+ on your
PATHif you build the MCP bridge from sourceGit,
curl, and a shellNode.js with
npxfor the reference filesystem serverOptional:
uvoruvxfor the Git MCP serverOptional: Homebrew on macOS for packaged
jdtlsBasic familiarity with Maven, Git, and layered Java applications
Project Setup
We start with the upstream Cargo Tracker repository. The first rule of this whole article is simple: the project must build before the assistant touches it. A language server does not rescue a broken classpath. An MCP bridge does not fix dependency resolution. If ./mvnw compile fails, the rest of the harness only gives you better-informed confusion.
Let’s create a working directory:
mkdir ai-tooling-example
cd ai-tooling-exampleClone the project and prove it compiles:
git clone https://github.com/eclipse-ee4j/cargotracker.git \
&& cd cargotracker \
&& ./mvnw -q -DskipTests compileThis repository is a good training ground because it looks like a real enterprise application. It has a layered structure, real domain boundaries, Jakarta EE APIs, and enough moving parts that a coding assistant can easily get lost without help.
You get a standard Maven layout with pom.xml at the root, Java sources under src/main/java, and tests under src/test/java. More importantly, you get existing application, domain, infrastructure, and interface packages. That matters. We want the assistant to navigate those packages. We do not want it to invent fresh package roots because it did not see what already exists.
If the build fails here, stop and fix Java, Maven, or network access first. That is not a side issue. It is part of the environment. A harness built on a non-working repository just gives you better tools for generating bad output.
Install and Validate JDTLS
We need the Eclipse JDT Language Server because that is the part that understands Java symbols, definitions, references, classpath resolution, and project structure. Your IDE already relies on this kind of capability. We are moving that same capability into the assistant’s tool loop.
On macOS, the easy path is Homebrew:
brew install jdtls
brew info jdtlsIf you want a manual install that works across operating systems, download the latest snapshot tarball from Eclipse and unpack it into a local directory:
JDTLS_TGZ=$(curl -fsSL https://download.eclipse.org/jdtls/snapshots/latest.txt)
curl -fLO "https://download.eclipse.org/jdtls/snapshots/${JDTLS_TGZ}"
mkdir -p ~/.local/jdtls && tar -xzf "${JDTLS_TGZ}" -C ~/.local/jdtlsAfter unpacking, you should see a plugins/ directory and one platform-specific configuration directory such as config_mac, config_linux, or similar. That platform directory matters. If you point jdtls at the wrong one, startup fails with OSGi errors that look confusing and unrelated to the real issue.
Here is a manual smoke test for macOS. On Linux, replace config_mac with config_linux.
LAUNCHER_JAR=$(ls ~/.local/jdtls/plugins/org.eclipse.equinox.launcher_*.jar | head -1)
java \
-Declipse.application=org.eclipse.jdt.ls.core.id1 \
-Dosgi.bundles.defaultStartLevel=4 \
-Declipse.product=org.eclipse.jdt.ls.core.product \
-Xmx1G -XX:+UseG1GC \
-jar "${LAUNCHER_JAR}" \
-configuration ~/.local/jdtls/config_mac \
-data /tmp/jdtls-workspace-cargotrackerThe -data directory is not your Git checkout. This is important. It is the language server’s own workspace cache and metadata area. The actual project path gets passed later through the language server handshake when the MCP bridge initializes the session.
A one-gigabyte heap is a good starting point for Cargo Tracker. You can reduce it later if you measure idle usage and know you have margin. But starting too small creates a different class of problem: slow indexing, unstable analysis, or random failures that look like language-server bugs when the real issue is starvation.
What does this give us? It gives the assistant symbol-level reality. jdtls knows what CargoRepository actually is. It knows where a method is declared, who references it, and how the classpath resolves imports. Without that, the assistant is doing fancy autocomplete over prose. With it, the assistant is navigating the same semantic graph your IDE uses.
It still has limits. It does not know your team’s conventions. It does not know what layers are socially forbidden. It does not know whether adding Jackson is acceptable just because a classpath contains it somewhere. That is why we need more than one server.
Build the LSP4J-MCP Bridge
Now we need a bridge between the Model Context Protocol world and the Java language server world. In this setup, we use LSP4J-MCP, which starts jdtls as a child process and exposes a smaller, controlled tool surface to the assistant.
Clone and build it:
git clone https://github.com/stephanj/LSP4J-MCP.git
cd LSP4J-MCP
mvn -q clean package -DskipTests
ls target/lsp4j-mcp-*.jarAt the time of writing, the project typically produces a shaded JAR with a name like lsp4j-mcp-1.0.0-SNAPSHOT.jar, but do not hardcode the exact version in your head. The safe habit is to inspect the target/ directory and then copy the resolved artifact into a stable project-local path.
Create local tool and log directories in Cargo Tracker, then copy the built JAR:
mkdir -p /path/to/cargotracker/.bob/tools /path/to/cargotracker/.bob/logs
cp target/lsp4j-mcp-*.jar /path/to/cargotracker/.bob/tools/lsp4j-mcp.jarWe give it a fixed local name, lsp4j-mcp.jar, because this is the name Bob will use from the committed configuration. This avoids rewriting config every time the bridge version changes.
You can do a standalone smoke launch before wiring Bob to it:
java -jar /path/to/cargotracker/.bob/tools/lsp4j-mcp.jar \
/path/to/cargotracker \
jdtlsLet that process sit for a while on first boot. The first run imports the Maven model and indexes the workspace. On a healthy setup, stderr shows project import progress or indexing activity. A broken setup exits immediately or throws classpath, Java version, or process launch errors.
This bridge is intentionally small. That is one of its strengths. It does not try to surface every possible LSP request. It exposes a smaller set of tools that are easy to review and safe to auto-approve in read-only mode. That smaller surface area is good for production teams because it limits accidental behavior and keeps the assistant’s tool menu understandable.
Typical tools exposed by this bridge include:
find_symbolsfind_referencesfind_definitiondocument_symbolsfind_interfaces_with_method
The exact names depend on the version you built, so always check the startup log or the bridge documentation before you finalize autoApprove. This is one of those details teams skip, and then they wonder why Bob keeps asking for approval on every call or fails because the configured tool name does not exist.
What does this bridge guarantee? It gives you language-aware discovery. That is the big win. What it does not guarantee is correctness of architecture or intent. It can tell the assistant where a class lives. It cannot tell the assistant whether adding a new service in that package is the right move. We still need explicit conventions and a human checkpoint for that.
Commit Project-Local MCP Configuration
The next step is the piece many teams miss. Do not leave the harness in somebody’s head or in a private desktop configuration. Commit it with the repository.
IBM Bob can read project-level MCP settings from .bob/mcp.json. That makes the environment reproducible. A teammate can clone the repository, open it, and inherit the same harness instead of reverse-engineering your local setup from screenshots and Slack messages.
First, make sure local logs stay out of version control:
printf '%s\n' '.bob/logs/' >> .gitignoreNow create .bob/mcp.json at the repository root:
{
"mcpServers": {
"java-lsp": {
"type": "stdio",
"command": "java",
"args": [
"-jar",
"${workspaceFolder}/.bob/tools/lsp4j-mcp.jar",
"${workspaceFolder}",
"jdtls"
],
"env": {
"LOG_FILE": "${workspaceFolder}/.bob/logs/jdtls-mcp.log",
"JAVA_HOME": "/Library/Java/JavaVirtualMachines/temurin-21.jdk/Contents/Home"
},
"autoApprove": [
"find_symbols",
"find_references",
"find_definition",
"document_symbols",
"find_interfaces_with_method"
],
"disabled": false
},
"filesystem": {
"type": "stdio",
"command": "npx",
"args": [
"-y",
"@modelcontextprotocol/server-filesystem@latest",
"${workspaceFolder}/src"
],
"autoApprove": [
"read_file",
"list_directory",
"search_files"
],
"disabled": false
},
"git": {
"type": "stdio",
"command": "uvx",
"args": [
"mcp-server-git",
"--repository",
"${workspaceFolder}"
],
"autoApprove": [
"git_log",
"git_diff",
"git_show"
],
"disabled": false
}
}
}This file is small, but it changes how the assistant behaves in a big way. Now Bob has three different ways to ground itself.
The java-lsp server answers semantic questions. It knows where symbols are defined and how code relates.
The filesystem server answers raw text questions. It can read files, list directories, and search source content.
The git server answers historical questions. It can show diffs, recent changes, and implementation intent from the repository history.
Together, those three servers approximate what a senior developer does mentally before touching code.
There are a few details here worth slowing down for.
The type is stdio for all three servers. That means Bob launches child processes and speaks MCP over standard input and output. This is simple and reliable. It also means broken command paths fail fast.
The ${workspaceFolder} variable matters a lot. Hard-coded local paths break the setup for everyone else. If your Bob release uses a different token, update it once in the committed config and document it. Do not hide that difference in tribal knowledge.
The LOG_FILE environment variable is a support tool. When something goes wrong, you want one place to tail logs. A missing log directory is not fatal in every setup, but it makes debugging harder and pushes errors into stderr where they get lost.
The JAVA_HOME setting is convenient and fragile at the same time. The example above is a macOS Temurin path. That is fine for a single-machine demo, but teams usually want one of two approaches. Either keep separate snippets for macOS and Linux in an internal doc, or remove JAVA_HOME from the committed file and rely on the parent environment. The important thing is to be explicit. Wrong JAVA_HOME values produce class version or startup errors that look like project bugs even though the problem is just the runtime.
The autoApprove lists deserve security thinking. These are read-oriented tools only. Keep it that way unless you have a very deliberate reason to expose write tools. The moment you auto-approve a mutating tool, you expand the assistant’s blast radius.
The filesystem scope is one of the most important design choices in the whole article. We point it at ${workspaceFolder}/src, not the whole repository. That is deliberate. Yes, it costs some convenience. No, the assistant cannot casually open README.md or inspect build output or local scratch files. That is the point. Narrow scope reduces accidental exposure of secrets, noisy directories, and irrelevant files.
The package reference uses @latest in this example because it is the easiest way to show the setup. In a team setting, pin it after validation. Cold-starting against whatever the registry says is “latest” makes laptops drift and turns debugging into archaeology.
The git server is optional, but it adds real value. Recent history often tells you which test file was changed last for a similar feature, which class is the real integration point, or which package is alive versus effectively abandoned. That kind of signal helps the assistant follow the grain of the codebase instead of fighting it.
Add a Repository Conventions File
Language tools tell the assistant what exists. They do not tell it what your team considers acceptable. That is why we add a committed AGENTS.md at the repository root.
Create AGENTS.md like this:
# Conventions
## Architecture
- Strict layering: interfaces → application → domain → infrastructure
- Domain types stay free of web and persistence annotations
- CDI constructor injection in application services
- Repositories are interfaces in domain; JPA implementations live in infrastructure
## Naming
- Application services: `*Service` under `application/internal/`
- REST facades: `*RestService` under `interfaces/rest/`
- JPA implementations: `Jpa*Repository`
## Serialization
- JSON via Jakarta JSON Binding in the stack versions Cargo Tracker already uses
- Keep serialization helpers out of domain entities unless the project explicitly allows it
## Testing
- Integration tests: `*IT.java`, follow Arquillian patterns already in the tree
- Unit tests: `*Test.java` with JUnit 5
- Reuse existing test data bootstrap patterns; do not invent a parallel database lifecycle
## Runtime descriptor and test packaging safety rules
Be careful with deployment descriptors and runtime-specific test resources.
Descriptor filenames, XML root elements, and schemas must match exactly.
Do not rename one descriptor type into another.
Do not package a standard `web.xml` file as `ibm-web-bnd.xml`.
Do not package a Liberty binding descriptor as `web.xml`.
When working with ShrinkWrap and Arquillian:
- inspect every file added through `addAsWebInfResource`
- confirm the source file content matches the target filename
- reuse existing repository examples before creating new descriptors
- prefer the minimal archive that works
- if no working example exists, stop and explain the uncertainty instead of inventing a runtime descriptor
Required self-check before finalizing test code:
- `web.xml` must contain the correct `web-app` root element
- `ibm-web-bnd.xml` must contain the correct Liberty binding root element
- no descriptor may be duplicated under the wrong target filename
- runtime-specific resources must follow existing repository conventions
## Runtime and verification
- For local runtime validation in this repository, use the Open Liberty profile
- Build with `./mvnw clean package -Popenliberty`
- Run with `./mvnw liberty:run -Popenliberty`
- Do not verify new REST behavior against a different runtime unless explicitly requestedThis file is simple governance. It does not try to explain the entire architecture. It sets boundaries. That is enough to stop a lot of common assistant mistakes.
For example, the classpath might contain something Jackson-related through another path or dependency. A generic model sees ObjectMapper and reaches for it. Your conventions file says no, this codebase uses Jakarta JSON Binding and serialization helpers stay out of domain entities. That single sentence removes a whole category of bad diffs.
Another common issue is layering drift. The assistant sees a problem, wants a quick helper, and places it in whatever package feels convenient. Your conventions file tells it where application services belong, where REST facades belong, and where repository implementations belong. That narrows the search space and makes planning better before implementation starts.
This file does not replace code review. It makes code review less repetitive and less reactive.
Build an Impact Map Before Writing Code
Here is the part that changes team behavior the most. The first thing you ask the assistant for is not code. It is a repository impact map.
This sounds slower. In practice, it is faster because it catches the expensive mistakes before they become patches.
Open Bob and start with a prompt like this:
Use the java-lsp and filesystem tools to inspect the Cargo Tracker project.
I want to add CSV export for cargo itineraries.
Produce a repository impact map: what must change, in which files, and which existing patterns to follow.
Annotate each bullet with the tool call that produced the evidence.
Do not write implementation code yet.A good response looks something like this:
# Repository Impact Map: CSV Export for Cargo Itineraries
## Evidence-Based Analysis
### 1. **Domain Layer** (No Changes Required)
**Evidence:** `java-lsp:document_symbols` on [`Itinerary.java`](src/main/java/org/eclipse/cargotracker/domain/model/cargo/Itinerary.java:19), [`Leg.java`](src/main/java/org/eclipse/cargotracker/domain/model/cargo/Leg.java:20), [`Cargo.java`](src/main/java/org/eclipse/cargotracker/domain/model/cargo/Cargo.java:58)
- Domain entities already expose all necessary data via getters
- [`Itinerary.getLegs()`](src/main/java/org/eclipse/cargotracker/domain/model/cargo/Itinerary.java:45) returns `List<Leg>`
- [`Leg`](src/main/java/org/eclipse/cargotracker/domain/model/cargo/Leg.java:20) exposes: voyage, loadLocation, unloadLocation, loadTime, unloadTime
- [`Cargo.getItinerary()`](src/main/java/org/eclipse/cargotracker/domain/model/cargo/Cargo.java:122) provides access to itinerary
### 2. **Application Layer** (No Changes Required)
**Evidence:** `read_file` on [`BookingService.java`](src/main/java/org/eclipse/cargotracker/application/BookingService.java:13), [`CargoRepository.java`](src/main/java/org/eclipse/cargotracker/domain/model/cargo/CargoRepository.java:5)
- [`CargoRepository.findAll()`](src/main/java/org/eclipse/cargotracker/domain/model/cargo/CargoRepository.java:9) already exists for bulk operations
- [`CargoRepository.find(TrackingId)`](src/main/java/org/eclipse/cargotracker/domain/model/cargo/CargoRepository.java:7) for single cargo lookup
### 3. **Interface Layer - NEW REST Endpoint** (CREATE)
**Evidence:** `read_file` on [`HandlingReportService.java`](src/main/java/org/eclipse/cargotracker/interfaces/handling/rest/HandlingReportService.java:26) shows REST pattern
**File:** `src/main/java/org/eclipse/cargotracker/interfaces/booking/rest/CargoItineraryExportService.java` (NEW)
- Follow pattern from [`HandlingReportService`](src/main/java/org/eclipse/cargotracker/interfaces/handling/rest/HandlingReportService.java:26):
- Use `@Stateless` annotation
- Use `@Path("/booking")` for base path
- Use `@GET` with `@Produces("text/csv")`
- Inject [`CargoRepository`](src/main/java/org/eclipse/cargotracker/domain/model/cargo/CargoRepository.java:5)
- Endpoints:
- `GET /booking/cargos/{trackingId}/itinerary/export` - single cargo
- `GET /booking/cargos/itineraries/export` - all cargos
### 4. **Interface Layer - CSV Assembler** (CREATE)
**Evidence:** `java-lsp:find_symbols` query "Assembler" found [`CargoRouteDtoAssembler`](src/main/java/org/eclipse/cargotracker/interfaces/booking/facade/internal/assembler/CargoRouteDtoAssembler.java:15), `read_file` showed assembler pattern
**File:** `src/main/java/org/eclipse/cargotracker/interfaces/booking/rest/assembler/ItineraryCsvAssembler.java` (NEW)
- Follow pattern from [`CargoRouteDtoAssembler`](src/main/java/org/eclipse/cargotracker/interfaces/booking/facade/internal/assembler/CargoRouteDtoAssembler.java:15):
- Use `@ApplicationScoped`
- Constructor injection with `@Inject`
- Methods:
- `String toCsv(Cargo cargo)` - single cargo to CSV
- `String toCsv(List<Cargo> cargos)` - multiple cargos to CSV
- CSV Format (based on [`Leg`](src/main/java/org/eclipse/cargotracker/domain/model/cargo/Leg.java:20) structure):
```
TrackingId,LegNumber,VoyageNumber,LoadLocation,LoadLocationName,UnloadLocation,UnloadLocationName,LoadTime,UnloadTime
```
### 5. **Dependencies** (No Changes Required)
**Evidence:** `read_file` on [`pom.xml`](pom.xml:1-100)
- Jakarta EE 10 already includes necessary APIs
- No additional CSV library needed (use standard Java StringBuilder/StringWriter)
- JSON Binding already available via Jakarta JSON Binding
### 6. **Existing Patterns to Follow**
#### REST Service Pattern
**Evidence:** [`HandlingReportService.java`](src/main/java/org/eclipse/cargotracker/interfaces/handling/rest/HandlingReportService.java:26)
```java
@Stateless
@Path("/handling")
public class HandlingReportService {
@Inject private ApplicationEvents applicationEvents;
@POST
@Path("/reports")
@Consumes({"application/json", "application/xml"})
public void submitReport(...) { }
}
```
#### Assembler Pattern
**Evidence:** [`CargoRouteDtoAssembler.java`](src/main/java/org/eclipse/cargotracker/interfaces/booking/facade/internal/assembler/CargoRouteDtoAssembler.java:15)
```java
@ApplicationScoped
public class CargoRouteDtoAssembler {
@Inject private LocationDtoAssembler locationDtoAssembler;
public CargoRoute toDto(Cargo cargo) {
// Transform domain to DTO
}
}
```
#### Data Access Pattern
**Evidence:** [`DefaultBookingServiceFacade.java`](src/main/java/org/eclipse/cargotracker/interfaces/booking/facade/internal/DefaultBookingServiceFacade.java:88-94)
```java
@Inject private CargoRepository cargoRepository;
public List<CargoRoute> listAllCargos() {
List<Cargo> cargos = cargoRepository.findAll();
// Transform and return
}
```
### 7. **Architecture Compliance**
**Evidence:** `read_file` on [`AGENTS.md`](AGENTS.md) conventions
- ✅ Strict layering maintained: interfaces → application → domain
- ✅ REST service in `interfaces/booking/rest/` (new package)
- ✅ Assembler in `interfaces/booking/rest/assembler/` (new package)
- ✅ CDI constructor injection
- ✅ Domain types remain annotation-free
- ✅ No new application services needed
## Summary
**Files to CREATE:**
1. `src/main/java/org/eclipse/cargotracker/interfaces/booking/rest/CargoItineraryExportService.java`
2. `src/main/java/org/eclipse/cargotracker/interfaces/booking/rest/assembler/ItineraryCsvAssembler.java`
**Files to MODIFY:** None
**Pattern Sources:**
- REST endpoint: [`HandlingReportService.java`](src/main/java/org/eclipse/cargotracker/interfaces/handling/rest/HandlingReportService.java:26)
- Assembler: [`CargoRouteDtoAssembler.java`](src/main/java/org/eclipse/cargotracker/interfaces/booking/facade/internal/assembler/CargoRouteDtoAssembler.java:15)
- Repository usage: [`DefaultBookingServiceFacade.java`](src/main/java/org/eclipse/cargotracker/interfaces/booking/facade/internal/DefaultBookingServiceFacade.java:88-94)
**Testing:**
- Integration test following Arquillian pattern in `src/test/java/`
- Verify with Open Liberty: `./mvnw liberty:run -Popenliberty`The exact class names and file paths depend on what the assistant finds in your checkout. The shape is what matters. You want real files, real existing types, and evidence from tool calls. Not “I would create a new controller package.” Not “It may be useful to add a DTO.” Evidence first.
Why does this matter? Because wrong-layer changes are cheap to fix at the map stage and expensive to fix after implementation starts. If the impact map shows an invented package, a duplicate service, or the wrong test class, you correct it in minutes. If you wait until after the assistant generated a patch, now you are reviewing code, logic, test strategy, and architecture drift at the same time.
This is the point where the human stays in charge. The assistant explores. You approve the shape.
Turn the Impact Map Into a Structured Task
After the impact map is approved, you convert it into an implementation contract. This is where you stop vague prompting and start being explicit about file boundaries, behavior, and acceptance criteria.
Use a task prompt like this:
Use the approved repository impact map below as the implementation contract.
Implement CSV export for cargo itineraries in the Cargo Tracker project.
Before you implement anything, read `AGENTS.md` and `CONVENTIONS.md` if present, and extract the rules that apply to this change.
List those rules first under a heading `Applicable conventions`.
Use those conventions as binding constraints for implementation and tests.
If the impact map conflicts with `AGENTS.md` or `CONVENTIONS.md`, stop and explain the conflict before writing code.
Important:
- Stay within the approved impact map
- Do not invent additional packages, layers, or abstractions
- Do not move this through a facade or application service
- Do not modify existing files unless a minimal compile-time change is strictly required
- If you believe an existing file must be modified, explain why before showing code
- Start by listing the exact files you will create or modify
- Then implement the change directly
## Approved scope
### Files to create
1. `src/main/java/org/eclipse/cargotracker/interfaces/booking/rest/CargoItineraryExportService.java`
2. `src/main/java/org/eclipse/cargotracker/interfaces/booking/rest/assembler/ItineraryCsvAssembler.java`
### Files to reference but not modify unless strictly required for compilation
- `src/main/java/org/eclipse/cargotracker/domain/model/cargo/Cargo.java`
- `src/main/java/org/eclipse/cargotracker/domain/model/cargo/Itinerary.java`
- `src/main/java/org/eclipse/cargotracker/domain/model/cargo/Leg.java`
- `src/main/java/org/eclipse/cargotracker/domain/model/cargo/CargoRepository.java`
- `src/main/java/org/eclipse/cargotracker/interfaces/handling/rest/HandlingReportService.java`
- `src/main/java/org/eclipse/cargotracker/interfaces/booking/facade/internal/assembler/CargoRouteDtoAssembler.java`
- `src/main/java/org/eclipse/cargotracker/interfaces/booking/facade/internal/DefaultBookingServiceFacade.java`
- `AGENTS.md`
- `CONVENTIONS.md`
## Required architecture
Follow the approved architecture exactly:
- Domain layer: no changes
- Application layer: no changes
- Interface layer: add one new REST service
- Interface layer: add one new assembler under `interfaces/booking/rest/assembler/`
Architecture constraints:
- Maintain strict layering
- Keep domain classes unchanged
- Use CDI constructor injection where conventions require it
- Keep the implementation in the interface layer
- No new facade methods
- No new application services
- No DTO layer for this feature
## Required REST endpoints
Create a new REST service class:
`src/main/java/org/eclipse/cargotracker/interfaces/booking/rest/CargoItineraryExportService.java`
Implementation requirements:
- Follow the REST structure pattern from `HandlingReportService`
- Use `@Stateless`
- Use base path `@Path("/booking")`
- Inject `CargoRepository`
- Return JAX-RS `Response`
- Produce `text/csv`
Add these endpoints exactly:
1. `GET /booking/cargos/{trackingId}/itinerary/export`
- export one cargo itinerary as CSV
2. `GET /booking/cargos/itineraries/export`
- export all cargo itineraries as CSV
Response requirements:
- Set `Content-Type` to `text/csv`
- Add `Content-Disposition` header so the CSV is downloadable
- Use clear file names for single-cargo and all-cargos export
## Required assembler
Create a new assembler class:
`src/main/java/org/eclipse/cargotracker/interfaces/booking/rest/assembler/ItineraryCsvAssembler.java`
Implementation requirements:
- Follow the assembler style from `CargoRouteDtoAssembler`
- Use `@ApplicationScoped`
- Use constructor injection with `@Inject`
- Work directly with domain objects
Required methods:
- `String toCsv(Cargo cargo)`
- `String toCsv(List<Cargo> cargos)`
CSV requirements:
- Use standard Java only
- No external CSV library
- No JSON or Jackson-based shortcut
- Build the CSV content with standard Java types
Use this exact column order:
`TrackingId,LegNumber,VoyageNumber,LoadLocation,LoadLocationName,UnloadLocation,UnloadLocationName,LoadTime,UnloadTime`
Behavior requirements:
- Include header row once
- For single cargo, output one row per itinerary leg
- For multiple cargos, output rows for all itinerary legs across all cargos
- Preserve the cargo tracking id on every row
- Handle cargos without itineraries in a way consistent with existing project behavior and explain the choice
## Data access requirements
Use existing repository methods only:
- `CargoRepository.find(TrackingId)` for single cargo export
- `CargoRepository.findAll()` for all-cargo export
Do not add repository methods.
Do not add application services to wrap repository access.
## Error handling
For the single-cargo endpoint:
- If the tracking id does not resolve to a cargo, follow the project’s existing not-found style
- Do not invent a new error format unless existing REST code clearly requires it
For the all-cargos endpoint:
- Return a valid CSV response even when there are no cargos
- Explain the behavior you chose
## Constraints
- No changes to domain classes
- No changes to application services
- No changes to facade interfaces or implementations
- No UI changes
- No new dependencies
- No unrelated refactoring
- No endpoint path changes
- No alternative package placement
## Testing
Add integration test coverage following the existing Arquillian style in `src/test/java/`.
Required tests:
1. Export single cargo itinerary as CSV
2. Return not-found behavior for unknown tracking id
3. Export all cargo itineraries as CSV
4. Return a valid CSV response shape for the empty-data case if applicable
Testing rules:
- Follow existing project conventions
- Keep tests minimal but real
- Assert content type
- Assert response status
- Assert header presence where relevant
- Assert CSV header row
- Assert at least one representative CSV row value
## Verification runtime
This feature must be verified using the Open Liberty profile.
Use these commands for final verification:
- `./mvnw clean test`
- `./mvnw clean package -Popenliberty`
- `./mvnw liberty:run -Popenliberty`
Do not use a different runtime profile for final verification.
## Output format
When you respond:
1. Show `Applicable conventions`
2. Show the exact files you will create or modify
3. Show the full code for each new or changed file
4. Show the full test code
5. Show any minimal deviation from the impact map
6. Show a `Conventions check` section against `AGENTS.md` and `CONVENTIONS.md`
7. End with the exact Maven verification commands using Open LibertyThis kind of prompt is much harder for the assistant to misunderstand. We define scope, file boundaries, constraints, and what “done” means. That reduces wandering. It also makes review easier because you can compare the resulting diff against the declared contract.
The practical difference is huge. “Implement CSV export” invites invention. A structured task grounded in a reviewed impact map invites extension of existing code.
Configure and Activate the Harness in Bob
At this point, the project-local configuration exists. The bridge JAR is in place. The assistant still needs to load and use the servers.
The exact Bob UI labels can change between releases, so do not get attached to the menu wording. What matters is that the workspace opens with the repository root and Bob loads .bob/mcp.json from that project.
When this works correctly, the assistant should expose the configured servers and tools without requiring a second round of manual setup. If it does not, treat that as a configuration mismatch or client-version mismatch, not as proof that the idea failed.
A good first smoke check inside Bob is intentionally tiny:
Call find_symbols with query "CargoRepository" and paste the first result path only.This test is good because it is narrow. It does not ask the assistant to think. It asks it to prove the tool wiring works.
If the response includes a real path under src/main/java, the semantic side of the harness is alive.
Next, test the filesystem scope:
Use the filesystem tool to read README.md at the repository root.If your filesystem server is correctly scoped to src/, this request should fail or return an out-of-scope error. That refusal is success. It proves your boundaries are working.
Then test Git context with something equally small:
Use git_log on the existing cargo facade integration test and summarize the most recent relevant change in one sentence.This tells you whether the assistant can consume project history without inventing it.
These tiny tests matter because they isolate failure. If you jump straight into a feature request and it goes wrong, you do not know whether the problem is the bridge, the server scope, the task prompt, the client, or the repository. Small smoke tests make the failure visible sooner.
Production Hardening
A harness that works on one laptop is not enough. We need to think about what happens when this setup becomes a team habit.
What happens under load
jdtls is not a trivial process. On multi-module repositories, it consumes real memory and CPU during indexing. Cargo Tracker is manageable, but bigger enterprise repositories expose the cost quickly. This is why we start with a one-gigabyte heap and why we keep a dedicated workspace cache.
If developers switch branches aggressively, the jdtls cache can become stale or noisy. When that happens, the assistant starts returning confusing symbol results or slow responses. The fix is operational, not architectural: use a dedicated cache location and be willing to wipe it when the workspace state is corrupted.
For example, you can move from /tmp to a stable directory:
mkdir -p ~/.cache/jdtls-cargotrackerThen update your launch configuration to use that directory as the -data location. This makes indexing more stable across sessions and gives you one place to clean up when the cache becomes suspect.
Security and blast radius
Read-only behavior is not automatic. It is designed.
Git MCP servers often expose both read and write operations. Filesystem servers expose whatever path you give them. If you point the filesystem server at ${workspaceFolder}, you are giving the assistant visibility into everything under the repository root, including local experiments, build output, or accidentally committed secrets.
That is why this tutorial scopes the filesystem server to src/. It is a deliberate loss of convenience in exchange for a smaller blast radius.
Pinning tool versions is part of the same story. @latest and ephemeral uvx installs are convenient for first setup. They are not good long-term operational defaults. Once the team validates a version combination, pin it and record it. Otherwise, you will debug “AI behavior changes” that are really dependency drift in supporting tools.
Portability and machine-specific configuration
A committed .bob/mcp.json is good. A committed file with a macOS-only JAVA_HOME path is half-good.
Teams usually solve this one of two ways. The first way is to keep the committed file generic and rely on inherited environment variables for Java. The second way is to maintain two small documented variants internally, one for macOS and one for Linux. The wrong answer is leaving a personal desktop path in the repo and hoping nobody else notices.
You should also think about whether Bob itself runs on the host, inside a dev container, or through a remote development path. That changes path semantics and environment inheritance. The harness still works, but you want to be clear about which process owns Java, which process sees the workspace root, and where logs land.
Supply chain risk in helper tools
npx and uvx are useful because they remove friction. They also resolve packages at invocation time unless pinned. That means the harness can change underneath you without a repository diff.
This is not just theoretical. Tool name changes, dependency updates, or package behavior differences can silently change what Bob sees. In a solo workflow that is annoying. In a shared workflow it becomes a support problem.
A practical team response is simple. Pin versions after validation. Record the version set in a small internal note or in the repository docs. Review upgrades like you would any other tooling change.
Human review boundaries
The biggest mistake teams make with this setup is thinking better tools remove the need for review. They do not. They move review earlier and make it cheaper.
The impact map is the first checkpoint. The structured task is the second. The code review is still there after that. What changes is the quality of the diff reaching review. You get fewer invented endpoints, fewer package mistakes, and fewer changes that violate conventions simply because the assistant had a smaller, better-defined space to operate in.
Verification
Now let’s verify the whole setup step by step.
Check Java and the Cargo Tracker build
Run this from the repository root:
cd /path/to/cargotracker
java -version
./mvnw clean package -PopenlibertyExpected result: java -version reports Java 21 or later, and Maven completes without BUILD FAILURE.
This verifies the foundation. If this step fails, none of the assistant tooling is trustworthy because the project state itself is broken.
Check Bob can call semantic tools
In Bob, send this prompt:
Call find_symbols with query "CargoRepository" and paste the first result path only.Expected result: a real path inside src/main/java, something along these lines:
src/main/java/org/eclipse/cargotracker/domain/model/cargo/CargoRepository.javaThe exact package may differ depending on the project revision, but it must point into the actual source tree.
This verifies that Bob can launch the bridge, the bridge can talk to jdtls, and the assistant can receive the result.
Check the filesystem boundary
In Bob, send this prompt:
Use the filesystem tool to read README.md from the repository root.Expected result: a refusal, an out-of-scope error, or a message indicating the file is outside the allowed path.
This verifies that your filesystem scope is doing what you intended. If Bob reads the file successfully, your path is too wide for the harness described in this tutorial.
Check Git context
In Bob, send this prompt:
Use git_log on the cargo facade integration test and summarize the most recent relevant change in one sentence.Expected result: a short answer grounded in actual commit history.
This verifies that the assistant can bring recent repository history into its planning loop. That matters when a feature needs to follow an existing testing style or recent implementation pattern.
Check the planning workflow
Now run the real planning test:
Use the java-lsp and filesystem tools to inspect the Cargo Tracker project.
I want to add CSV export for cargo itineraries.
Produce a repository impact map with files, layers, and existing patterns to follow.
Annotate each bullet with the tool call that produced the evidence.
Do not write code yet.Expected result: a grounded map that names existing files and classes, stays inside existing package structure, and shows evidence.
This is the real proof that the harness works. Not just that the tools launch, but that the assistant changes behavior and plans against repository reality.
Architecture Recap
From the assistant’s point of view, the setup looks like this:
IBM Bob
├── java-lsp (LSP4J-MCP to jdtls)
│ find_symbols / find_references / find_definition / document_symbols
├── filesystem (@modelcontextprotocol/server-filesystem on src/)
│ read_file / list_directory / search_files
└── git (mcp-server-git)
git_log / git_diff / git_showEach server answers a different question.
The Java language server answers, “What does this code mean?”
The filesystem server answers, “What is actually written in the allowed source tree?”
The Git server answers, “What changed recently, and what implementation history should we respect?”
That split is the whole point. A single prompt is not enough. Good code generation needs semantic context, textual context, and often historical context.
Further Reading
If you want to continue from here, the next useful documents are the IBM Bob documentation for project-level MCP configuration, the Cargo Tracker repository itself for understanding the domain and package layout, and the LSP4J-MCP project for the exact bridge behavior and supported tool names.
You should also keep your own small internal note for version combinations that your team has validated. This sounds boring. It saves time.
Conclusion
We built a small but practical harness around Cargo Tracker so the assistant can plan against classpath reality, stay inside a controlled source boundary, and use repository history when it matters. That changes the quality of AI-generated work because it removes the assistant’s need to guess about symbols, layers, and existing patterns. The real lesson is not specific to Cargo Tracker or IBM Bob. AI coding quality follows environment quality.


