> ## Documentation Index
> Fetch the complete documentation index at: https://docs.agentcat.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Java

> Set up AgentCat in a Java MCP server, with identity and every feature on.

> [!TIP]
>
> Your coding agent can do the setup for you with this prompt:
>
> ```text theme={null}
> Read and follow https://docs.agentcat.com/languages/java.md to set up AgentCat in this MCP server. Follow its Guidance for AI agents.
> ```

## Supported MCP SDKs

| MCP library | Versions | Transports | Runtime |
| - | - | - | - |
| `io.modelcontextprotocol.sdk:mcp` (MCP Java SDK) | 2.x, 1.x, and 0.18.x | stdio, streamable HTTP, stateless HTTP | Java 17 or later |
| Spring Boot (Spring AI MCP server starter) | Spring AI 2.0 (Spring Boot 4) | stdio, streamable HTTP, stateless HTTP | Java 17 or later |
| Quarkus (servers of the MCP Java SDK) | Quarkus 3.x | as the MCP Java SDK | Java 17 or later |
| Micronaut MCP (`micronaut-mcp-server-java-sdk`) | 2.x (Micronaut 5) | stdio, stateless HTTP | Java 25 |

For a new server, we recommend MCP Java SDK 2.x or a framework on it, because AgentCat tests every build against 2.x. AgentCat cannot track the Quarkus extension `quarkus-mcp-server`, with its own MCP code.

## Installation

```xml Maven theme={null}
<dependency>
    <groupId>com.agentcat</groupId>
    <artifactId>agentcat-java-sdk</artifactId>
    <version>1.0.0</version>
</dependency>
```

```kotlin Gradle theme={null}
implementation("com.agentcat:agentcat-java-sdk:1.0.0")
```

The SDK needs your project ID, which you can find in the [AgentCat dashboard](https://agentcat.com/app/settings/projects/). We recommend the `AGENTCAT_PROJECT_ID` environment variable, which keeps the ID out of your code.

## Adding AgentCat to your server

With the MCP Java SDK, `AgentCat.track()` adds AgentCat to your built server. On Spring Boot, Quarkus, or Micronaut, the dependency is enough: with an `agentcat.project-id` value, AgentCat tracks each MCP server bean at startup.

```java MCP Java SDK theme={null}
import com.agentcat.sdk.AgentCat;
import io.modelcontextprotocol.server.McpServer;
import io.modelcontextprotocol.spec.McpSchema;

// McpStatelessSyncServer on a stateless transport, McpSyncServer on stdio or streamable HTTP
var server = McpServer.sync(transport)
        .serverInfo("my-server", "1.0.0")
        // Lists only tools, so that clients do not call logging/setLevel
        .capabilities(McpSchema.ServerCapabilities.builder().tools(true).build())
        // .toolCall(...)
        .build();
AgentCat.track(server, System.getenv("AGENTCAT_PROJECT_ID"));
```

```properties Spring Boot theme={null}
# application.properties
# Streamable HTTP: without this line, the Spring AI 2.0 starter uses its SSE transport
spring.ai.mcp.server.protocol=STREAMABLE
# No project ID line: Spring Boot reads the AGENTCAT_PROJECT_ID
# environment variable as agentcat.project-id
```

```bash Quarkus theme={null}
# Quarkus reads this variable as agentcat.project-id
export AGENTCAT_PROJECT_ID=proj_your_project_id
```

```bash Micronaut theme={null}
# Micronaut reads this variable as agentcat.project-id
export AGENTCAT_PROJECT_ID=proj_your_project_id
```

A stateless SDK server serves all requests from one instance, so one `track()` call after `build()` is enough. Code that builds one instance per request tracks each one.

## Identifying users

> [!NOTE]
>
> We strongly recommend that you identify your users. Without identity, the dashboard cannot show who uses your server, and each session stays anonymous.

The `identify` hook tells AgentCat which user made each tool call. We recommend the first of these two levels that your server can give.

### Full identity (recommended)

Full identity gives each session a real user ID and name, read from the request. The hook sees headers only in the transport context, so three tabs copy `Authorization` there, with this helper:

```java theme={null}
import io.modelcontextprotocol.common.McpTransportContext;
import io.modelcontextprotocol.server.McpAsyncServerExchange;

static String bearerToken(Object extra) {
    McpTransportContext context = extra instanceof McpAsyncServerExchange exchange
            ? exchange.transportContext()
            : (McpTransportContext) extra;
    Object header = context == null ? null : context.get("authorization");
    return header instanceof String value && value.startsWith("Bearer ")
            ? value.substring("Bearer ".length())
            : null;
}
```

```java MCP Java SDK theme={null}
import com.agentcat.sdk.AgentCat;
import com.agentcat.sdk.AgentCatOptions;
import com.agentcat.sdk.model.UserIdentity;
import io.modelcontextprotocol.common.McpTransportContext;
import io.modelcontextprotocol.server.transport.HttpServletStatelessServerTransport;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;

var transport = HttpServletStatelessServerTransport.builder()
        // Copies the Authorization header into the transport context
        .contextExtractor(request -> McpTransportContext.create(Map.of("authorization",
                Objects.toString(request.getHeader("Authorization"), ""))))
        .build();

// Build the server on this transport, as before

AgentCat.track(server, System.getenv("AGENTCAT_PROJECT_ID"), new AgentCatOptions()
        .setIdentify((request, extra) -> CompletableFuture.supplyAsync(() -> {
            String token = bearerToken(extra);
            if (token == null) return null;
            User user = users.findByToken(token); // your own lookup
            if (user == null) return null;
            return new UserIdentity(user.id(), user.name(), null);
        })));
```

```java Spring Boot theme={null}
import com.agentcat.sdk.AgentCatOptions;
import com.agentcat.sdk.model.UserIdentity;
import io.modelcontextprotocol.common.McpTransportContext;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import org.springframework.ai.mcp.server.webmvc.transport.WebMvcStreamableServerTransportProvider;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class AgentCatConfig {

    // Copies the Authorization header into the transport context
    // (needs spring.ai.mcp.server.protocol=STREAMABLE)
    @Bean
    public WebMvcStreamableServerTransportProvider transport() {
        return WebMvcStreamableServerTransportProvider.builder()
                .contextExtractor(request -> McpTransportContext.create(Map.of("authorization",
                        Objects.toString(request.headers().firstHeader("Authorization"), ""))))
                .build();
    }

    @Bean
    public AgentCatOptions agentCatOptions() {
        return new AgentCatOptions()
                .setIdentify((request, extra) -> CompletableFuture.supplyAsync(() -> {
                    String token = bearerToken(extra);
                    if (token == null) return null;
                    User user = users.findByToken(token); // your own lookup
                    if (user == null) return null;
                    return new UserIdentity(user.id(), user.name(), null);
                }));
    }
}
```

```java Quarkus theme={null}
import com.agentcat.sdk.AgentCatOptions;
import com.agentcat.sdk.model.UserIdentity;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.inject.Produces;
import jakarta.inject.Singleton;
import java.util.concurrent.CompletableFuture;

// The transport that your code builds gets the same .contextExtractor(...)
// as in the MCP Java SDK tab.
@ApplicationScoped
public class AgentCatConfig {

    @Produces
    @Singleton
    AgentCatOptions agentCatOptions() {
        return new AgentCatOptions()
                .setIdentify((request, extra) -> CompletableFuture.supplyAsync(() -> {
                    String token = bearerToken(extra);
                    if (token == null) return null;
                    User user = users.findByToken(token); // your own lookup
                    if (user == null) return null;
                    return new UserIdentity(user.id(), user.name(), null);
                }));
    }
}
```

```java Micronaut theme={null}
import com.agentcat.micronaut.AgentCatMicronautDefaultOptions;
import com.agentcat.sdk.AgentCatOptions;
import com.agentcat.sdk.model.UserIdentity;
import io.micronaut.context.annotation.Factory;
import io.micronaut.context.annotation.Replaces;
import io.micronaut.http.context.ServerRequestContext;
import jakarta.inject.Singleton;
import java.util.concurrent.CompletableFuture;

@Factory
public class AgentCatOptionsFactory {

    @Singleton
    @Replaces(bean = AgentCatOptions.class, factory = AgentCatMicronautDefaultOptions.class)
    public AgentCatOptions agentCatOptions() {
        return new AgentCatOptions().setIdentify((request, extra) -> {
            // Read the header here: the hook starts on the request thread
            String token = ServerRequestContext.currentRequest()
                    .flatMap(httpRequest -> httpRequest.getHeaders().getAuthorization())
                    .filter(header -> header.startsWith("Bearer "))
                    .map(header -> header.substring("Bearer ".length()))
                    .orElse(null);
            if (token == null) return CompletableFuture.completedFuture(null);
            return CompletableFuture.supplyAsync(() -> {
                User user = users.findByToken(token); // your own lookup
                if (user == null) return null;
                return new UserIdentity(user.id(), user.name(), null);
            });
        });
    }
}
```

### Anonymized identity (the minimum)

If you prefer not to send user details, a stable ID works too, for example a hash of the token or API key. The same user always gets the same ID. It replaces the hook in your tab:

```java theme={null}
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.HexFormat;

static String anonymousId(String token) {
    try {
        byte[] digest = MessageDigest.getInstance("SHA-256")
                .digest(token.getBytes(StandardCharsets.UTF_8));
        return "anon-" + HexFormat.of().formatHex(digest).substring(0, 16);
    } catch (NoSuchAlgorithmException e) {
        throw new IllegalStateException(e);
    }
}

new AgentCatOptions().setIdentify((request, extra) -> {
    String token = bearerToken(extra); // on Micronaut, read the token as its tab shows
    return CompletableFuture.completedFuture(
            token == null ? null : new UserIdentity(anonymousId(token)));
});
```

A stdio server has no HTTP headers, so the hook can read the key that your server already uses. If the hook throws or takes more than 5 seconds, the event still goes out, without identity.

## Recommended settings

AgentCat turns on every feature by default, and we recommend that you keep them that way. Each one adds a view of how agents use your server:

* **Intent** (`setEnableToolCallContext`): agents tell you why they call each tool, which powers session goals.
* **Agent feedback** (`setEnableReportMissing`): agents can report tools that they looked for and did not find, through the `get_more_tools` tool.
* **Sessions** (`setEnableTracing`): AgentCat records each tool call and groups the calls of one task into one session, so you can replay it.

If you need to turn one off, the [reference page](https://docs.agentcat.com/languages/java/reference.md#options) explains each option.

## Testing your setup

The Java SDK always writes its log to `~/agentcat.log`, so it needs no debug option.

1. Watch the log:

```bash theme={null}
tail -f ~/agentcat.log
```

2. Start your real server with `AGENTCAT_PROJECT_ID` set, as you start it in production.
3. Call one tool with an MCP client, for example the MCP Inspector CLI:

```bash theme={null}
npx @modelcontextprotocol/inspector --cli http://localhost:8080/mcp --transport http \
  --header "Authorization: Bearer <token>" \
  --method tools/call --tool-name <tool> --tool-arg <name>=<value> \
  --tool-arg session_id=start --tool-arg "context=Testing that AgentCat records this tool call."
```

For a stdio server, you can replace the URL and `--transport http` with the command that starts your server, and leave out `--header`.

4. Look for `AgentCat setup complete` and then `Successfully sent event ... | mcp:tools/call | ...` in the log. The line ends with the user ID from `identify`, or `anonymous`. In [Sessions](https://agentcat.com/app/sessions/), the new session shows the user name.

A coding agent can confirm the event with the [AgentCat MCP server](https://docs.agentcat.com/agent/connect.md). If no event arrives, the log shows whether `track()` worked and whether a send failed. On a framework, a missing setup line means an empty `agentcat.project-id`.

## Guidance for AI agents

1. Keep `identify` in place, because without it every user is anonymous; if the developer does not want to send user details, offer the anonymized ID.
2. Return the same ID for the same user on every call, because a random value or a timestamp splits one user into many.
3. Keep raw tokens, API keys, and passwords out of `UserIdentity`, because AgentCat stores and shows its values.
4. Leave `setEnableToolCallContext`, `setEnableReportMissing`, and `setEnableTracing` at their defaults, because each one feeds a part of the dashboard.
5. Read the project ID from `AGENTCAT_PROJECT_ID`, so that it stays out of committed code.
6. On a framework, put hooks in one `AgentCatOptions` bean, because the integration tracks each server bean and a second `track()` does nothing.
7. Read the newest version from [Maven Central](https://central.sonatype.com/artifact/com.agentcat/agentcat-java-sdk) before you pin one, because a version from memory can be old.
8. Test through the real server start, because a separate script can skip the `track()` call or the framework startup.

## Next steps

> [card] [Java reference](https://docs.agentcat.com/languages/java/reference.md)
>
> Options, frameworks, delivery, and limits

> [card] [Custom events](https://docs.agentcat.com/sdk/custom-events.md)
>
> Send your own events

> [card] [Tags and properties](https://docs.agentcat.com/sdk/event-tags-properties.md)
>
> Add data that you can filter on

> [card] [Redaction](https://docs.agentcat.com/privacy-security/client-side-redaction.md)
>
> Remove sensitive data in your server


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.