Java Cup
Inside Java

News and views from members of the Java team at Oracle

Pairing In-Process and Hosted Embeddings for Java MCP Tool Development

During Java for an AI World - JavaOne 2026 Keynote, several speakers showed how innovations in the Java language and platform are relevant for AI, from libraries and model integration to retrieval and workflow patterns. One of those examples was a larger end-to-end support system built from several cooperating Java services: a patient fills in a complaint via a web application, that request becomes a helpdesk ticket, and an AI-based triage pipeline speeds up its solution. There was a lot to cover in the keynote, so the Model Context Protocol (MCP) server for urgency scoring only appeared briefly. This article covers the design decisions behind that MCP server and its possible role in the triage workflow: given a complaint message, how urgent is it?

Why Build an MCP Server?

The goal of the urgency-mcp is to expose one capability, so other components can send a complaint and receive an urgency score through a standard MCP interface. For this use case, MCP provides a stable way for other parts of the system to consume that capability. Instead of each caller depending on a custom REST endpoint, request format, or client implementation, it can rely on a standard tool contract: discover the server, find the tool, send the complaint text, and read the score.

With MCP, the integration is less tied to one service-specific API, becoming easier to change the consumer, or to move the urgency logic behind a different internal implementation without having to rewrite every caller. While MCP keeps the tool contract stable, the score path and result depend on the embedding model used and how the underlying score models were trained and exported.

How to Obtain Custom Score Models

The configuration of the urgency-mcp points to .dnet scorer files at runtime, requiring those during inference. These files are training outputs from urgency-training-pipeline and each one is valid only for the embedding representation it was trained on.

This training pipeline functions as follows:

  1. It loads ticket examples from training/dataset JSON files.

  2. It computes embeddings for the chosen provider.

  3. It caches embeddings under training/embeddings/{provider} so repeated runs do not recompute unchanged inputs.

  4. It shuffles the samples, splitting them 80/20 into training and validation sets.

  5. It trains DeepNetts feed-forward networks with shape inputDim -> 64 -> 32 -> 1. A feed-forward network is a simple neural network in which data moves layer by layer, from the input to the output, without loops or recurrence. In this case, the embedding vector enters the input layer, passes through two hidden layers of sizes 64 and 32, and ends in a single output value that represents the predicted urgency score.

  6. It exports provider-specific files such as model-scorer-local.dnet and model-scorer-openai.dnet.

The local training path uses in-process MiniLM embeddings through DJL:

sentence-transformers/all-MiniLM-L6-v2 
    -> 384-dimensional vectors 
        -> model-scorer-local.dnet

while the external provider-backed training path uses OpenAI embeddings:

text-embedding-3-small 
    -> 1536-dimensional vectors 
        -> model-scorer-openai.dnet

The same training pipeline produces two model families from each embedding provider: a regression scorer and a binary classifier. urgency-mcp uses the regression scorer files, such as model-scorer-local.dnet and model-scorer-openai.dnet, not the binary classifier. The regression path predicts urgency on a normalized 0..1 scale and is then mapped back to an urgency range for the MCP tool. The critical constraint is the provider compatibility: a scorer file is meaningful only for the embedding representation it was trained against. That is why the runtime configuration points to different scorer files for local and OpenAI modes.

One Tool, Two Inference Paths

In this example, the Java MCP implementation choice is based on Helidon. This framework provides support for building Model Context Protocol (MCP) servers through a dedicated extension. So bootstrapping an MCP server is mostly declarative: @Mcp.Path defines the endpoint, @Mcp.Server identifies the server, and @Mcp.Tool exposes the operation. These annotations mean that the framework handles the MCP-facing server surface, while the application code stays focused on the domain operation behind each tool.

The urgency-mcp uses that annotation-based MCP server pattern to expose the urgency scoring capability. The server declares @Mcp.Path, @Mcp.Server, and @Mcp.Tool to define where the MCP endpoint lives, how the server identifies itself, and which tool it exposes, in a way that follows the MCP protocol model. Behind that MCP boundary, the server delegates scoring to the local inference stack, which can use different embedding providers together with the matching scorer.

@Mcp.Path(McpUrgencyServer.MCP_PATH)
@Mcp.Server(McpUrgencyServer.MCP_SERVER_NAME)
final class McpUrgencyServer {
    // ...
    @Mcp.Tool(
            value = "Get urgency score (0-10) for a support ticket complaint",
            title = "Get urgency score",
            readOnlyHint = true,
            destructiveHint = false,
            idempotentHint = true,
            openWorldHint = false)
    McpToolResult getUrgency(@Mcp.Description("complaint text to score") String phrase) {
        double score = scorerSupplier.get().score(phrase);
        return McpToolResult.create(Double.toString(score));
    }
}

A client should not need to know which embedding provider is active. It should discover the server, inspect the tool metadata, send a complaint phrase, and receive a score. The hints inside the @Mcp.Tool annotation represent metadata for the MCP client:

Through the annotation model of @Mcp.Tool, everything inside the getUrgency method belongs to the inference layer, regardless of provider choice.

There is also an operational detail that determines the behavior at runtime of this service: McpUrgencyServer creates the scorer lazily through its supplier, so the MCP endpoint can come up before the full inference stack is exercised. That means the MCP server can start, while the first real tool call pays the cost of scorer construction. This approach separates two kinds of readiness:

With this distinction in mind, let's see why the implementation supports both in-process and external provider-backed embeddings for the same urgency capability.

Why Both In-Process and Provider-Backed Embeddings Exist

Keeping both in-process and external provider-backed embeddings serves two different engineering needs behind one stable MCP interface:

These embeddings represent two versions of the vector-generation stage behind the same tool:

MCP client
  -> getUrgency(phrase)
  -> embedding generator
  -> provider-compatible DeepNetts scorer
  -> urgency score

For the in-process provided embeddings, the runtime configuration is:

urgency:
  provider: local
  providers:
    local:
      model:
        name: model-scorer-local.dnet
        location: ../urgency/model
      embedding:
        name: sentence-transformers/all-MiniLM-L6-v2
        location: ../urgency/model
        dimensions: 384

In this scenario, urgency-mcp loads sentence-transformers/all-MiniLM-L6-v2 through DJL, expects 384-dimensional vectors, and applies the local DeepNetts scorer that was trained against that same representation. Furthermore, the local embedding generator is responsible for loading that model and enforcing the expected output size:

    private Predictor<String, float[]> loadPredictor() {
      try {
        Criteria<String, float[]> criteria = Criteria.builder()
                .setTypes(String.class, float[].class)
                .optModelUrls(DJL_MODEL_URL_PREFIX + modelName)
                .optEngine(ENGINE)
                .optTranslatorFactory(new TextEmbeddingTranslatorFactory())
                .build();
        model = criteria.loadModel();
        return model.newPredictor();
      } catch (ModelNotFoundException | MalformedModelException | IOException e) {
        throw new RuntimeException(
                "Failed to load local embedding model: " + modelName 
                        + " (configured location: " + modelLocation + ")",
                e);
      }
}

A local scorer trained on MiniLM embeddings is therefore matched at runtime with the same MiniLM embedding model.

For the hosted semantic provider case, the example uses OpenAI embeddings through text-embedding-3-small. The hosted provider supplies the vector, but the final urgency judgment still comes from DeepNetts.

urgency:
  provider: openai
  providers:
    openai:
      model:
        name: model-scorer-openai.dnet
        location: ../urgency/model
      embedding:
        model:
          name: text-embedding-3-small
        dimensions: 1536

At runtime, the complaint text is first sent to OpenAI that converts into a numeric embedding vector, and that vector is then passed to the local model-scorer-openai.dnet model, which computes the final urgency score returned by the server.

Vector Compatibility Is Not Enough

You probably noticed that the two models do not use the same vector size. In this project, the local path uses sentence-transformers/all-MiniLM-L6-v2, which produces 384-dimensional embeddings, smaller than the 1536-dimensional OpenAI ones.

A smaller vector does not automatically mean a worse scorer because that scorer was trained on that exact representation. If the training set is representative and the scorer validates well on held-out examples, then a 384-dimensional semantic embedding can absolutely produce useful urgency predictions. So, for local development, the goal of the local MiniLM-based scorer is not to mirror every aspect of the hosted embedding path, but to achieve the following:

When the local model is semantic, the scorer is trained against that representation, and the task is narrower than open-ended retrieval or general-purpose reasoning.

The local mode is useful for more than MCP wiring alone. It is a good fit for repeated tool calls, MCP metadata and discovery checks, scorer-loading verification, end-to-end local runs, broad scoring sanity checks, and integration testing across services using only local dependencies.

The external provider-backed mode should prove that the hosted embedding/scorer pair behaves semantically the way the system needs. Each provider should stay internally consistent with the scorer trained for it. If you notice that the in-process and external provider scores differ, that may simply reflect the fact that the representations capture different information.

Provider Choice Stays Out of the Tool Name

The server currently uses urgency.provider to select the active path and keep the choice behind the MCP boundary:

java -Durgency.provider=local -jar target/urgency-mcp.jar
java -Durgency.provider=openai -Dopenai.api-key="$OPENAI_API_KEY" -jar target/urgency-mcp.jar

Protocol shape remains fixed, while inference strategy can evolve behind it.

Final Thoughts

A two-provider design stays useful only if the differences remain visible. The local provider is a real semantic embedding path through MiniLM, but it is still a different provider from the hosted OpenAI path. The remote provider may be semantically stronger in some cases, but it can also be slower and operationally dependent on credentials and network access.

The in-process and provider-backed scorers must be trained separately. While the validation datasets may overlap, the expected behavior should account for provider-specific representation differences. Compare both providers on a small shared dataset, but a scorer should never be reused across incompatible embedding spaces. Each embedding/scorer combination has to be treated as its own deployable inference unit.

In development, use in-process embeddings for MCP protocol checks, adapter behavior, end-to-end scoring, and integration testing when the goal is to keep the system runnable without external dependencies. The hosted embeddings path is then useful for semantic validation, when credentials are available and when the point of the test is to evaluate the quality of that embedding model rather than basic integration.