Skip to main content
Glama
Log-LogN

langfuse-mcp-java

by Log-LogN
README.md
# Langfuse MCP Server — Java / Spring AI

![Spring Boot](https://img.shields.io/badge/Spring%20Boot-4.0.3-6DB33F?style=flat&logo=springboot&logoColor=white)
![Spring AI](https://img.shields.io/badge/Spring%20AI-2.0.0--M3-6DB33F?style=flat&logo=spring&logoColor=white)
![Java](https://img.shields.io/badge/Java-21-ED8B00?style=flat&logo=openjdk&logoColor=white)
![Lombok](https://img.shields.io/badge/Lombok-red?style=flat&logo=lombok&logoColor=white)

A production-grade MCP server that connects any MCP-compatible AI agent to your Langfuse observability data.  
Query traces, debug errors, inspect sessions, manage prompts, run evaluations, annotate data, and configure models — all through natural language.

> **Transport:** Streamable HTTP on port 8080, compatible with Cursor, Claude Desktop, VS Code / GitHub Copilot, and any MCP client that supports HTTP transport.

---

## Why this server?

| Capability | This server | Official Langfuse MCP |
|---|---|---|
| Traces & Observations | ✅ | ❌ |
| Sessions & Users | ✅ | ❌ |
| Exception tracking | ✅ | ❌ |
| Prompt management (read + write) | ✅ | ✅ read-only |
| Dataset & run management | ✅ | ❌ |
| Scores & score configs | ✅ | ❌ |
| Annotation queues | ✅ | ❌ |
| Comments | ✅ | ❌ |
| Model definitions | ✅ | ❌ |
| LLM connections | ✅ | ❌ |
| Project introspection | ✅ | ❌ |
| Schema introspection | ✅ | ❌ |
| Java / Spring AI | ✅ | ❌ (Python) |

---

## Prerequisites

- **Java 21** or later
- **Maven 3.9+** (or use the Docker build — no local Maven required)
- A [Langfuse](https://langfuse.com) account with an API key pair (`public-key` + `secret-key`)

---

## Quick Start

```bash
# 1. Build
mvn clean package -DskipTests

# 2. Set credentials
export LANGFUSE_PUBLIC_KEY=pk-lf-...
export LANGFUSE_SECRET_KEY=sk-lf-...
export LANGFUSE_HOST=https://cloud.langfuse.com

# 3. Run (Streamable HTTP transport — port 8080)
java -jar target/langfuse-mcp-1.0.0.jar

# 4. Verify
curl http://localhost:8080/actuator/health

# 5. Inspect all tools
npx @modelcontextprotocol/inspector http://localhost:8080/mcp
```

Get credentials from [Langfuse Cloud](https://cloud.langfuse.com) → Settings → API Keys.  
Self-hosted Langfuse? Set `LANGFUSE_HOST` to your instance URL.

---

## Configuration

All configuration is driven by environment variables (or `application.yml` for local overrides).

| Property | Env var | Required | Default | Description |
|---|---|---|---|---|
| `langfuse.public-key` | `LANGFUSE_PUBLIC_KEY` | ✅ | — | Langfuse project public key |
| `langfuse.secret-key` | `LANGFUSE_SECRET_KEY` | ✅ | — | Langfuse project secret key |
| `langfuse.host` | `LANGFUSE_HOST` | ✅ | — | Langfuse base URL, e.g. `https://cloud.langfuse.com` |
| `langfuse.timeout` | `LANGFUSE_TIMEOUT` | ❌ | `30s` | HTTP request timeout — Spring Duration format, e.g. `30s`, `1m`, `90s` |
| `langfuse.read-only` | — | ❌ | `true` | Informational flag; write operations are available through specific tools |

### Trailing slash handling

`LANGFUSE_HOST` may be specified with or without a trailing slash — the server normalises it automatically.

---

## Client Configuration

### Cursor (`.cursor/mcp.json`)

```json
{
  "mcpServers": {
    "langfuse": {
      "url": "http://localhost:8080/mcp"
    }
  }
}
```

### Claude Desktop (`claude_desktop_config.json`)

```json
{
  "mcpServers": {
    "langfuse": {
      "url": "http://localhost:8080/mcp"
    }
  }
}
```

On macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`

### VS Code / GitHub Copilot

**URL mode:**

```json
{
  "github.copilot.chat.mcp.servers": {
    "langfuse": {
      "url": "http://localhost:8080/mcp"
    }
  }
}
```

**Command mode** (stdio-only clients):

```json
{
  "github.copilot.chat.mcp.servers": {
    "langfuse": {
      "command": "java",
      "args": ["-jar", "/absolute/path/to/langfuse-mcp-1.0.0.jar"],
      "env": {
        "LANGFUSE_PUBLIC_KEY": "pk-lf-...",
        "LANGFUSE_SECRET_KEY": "sk-lf-...",
        "LANGFUSE_HOST": "https://cloud.langfuse.com"
      }
    }
  }
}
```

> **Note:** The MCP endpoint is `/mcp` (streamable HTTP). The legacy SSE `/sse` endpoint is not used by this server.

---

## Docker

The `Dockerfile` is a multi-stage build: it compiles the Spring Boot jar inside Docker and runs the MCP server on port `8080`. No local Maven installation is needed.

```bash
# Build image (compiles inside Docker)
docker build -t langfuse-mcp:latest .

# Run
docker run --rm -p 8080:8080 \
  -e LANGFUSE_PUBLIC_KEY=pk-lf-... \
  -e LANGFUSE_SECRET_KEY=sk-lf-... \
  -e LANGFUSE_HOST=https://cloud.langfuse.com \
  langfuse-mcp:latest
```

After the container starts:

| Endpoint | URL |
|---|---|
| Health check | `http://localhost:8080/actuator/health` |
| Ping | `http://localhost:8080/ping` |
| MCP endpoint | `http://localhost:8080/mcp` |

**Langfuse running in another container on the same host:**

```bash
-e LANGFUSE_HOST=http://host.docker.internal:3000
```

---

## Tools Reference (55 total)

Every tool returns a consistent `ApiResponse<T>` envelope:

```json
{ "success": true,  "data": { ... }, "timestamp": "2025-01-15T10:30:00Z" }
{ "success": false, "errorCode": "TRACE_NOT_FOUND", "errorMessage": "...", "timestamp": "..." }
```

Paginated list responses wrap their items in a `PagedResponse<T>`:

```json
{
  "data": [ ... ],
  "meta": { "page": 1, "limit": 20, "totalItems": 142, "totalPages": 8 }
}
```

Pagination is 1-based (`page` defaults to `1`). `limit` defaults to `20` and is capped at `100` where noted. To page through results, increment `page` while keeping `limit` fixed.

---

### Traces (8 tools)

| Tool | Description |
|---|---|
| `fetch_traces` | Paginated list of traces. Filter by `userId`, `name`, `sessionId`, `tags`, `fromTimestamp`, `toTimestamp`. |
| `fetch_trace` | Full detail of a single trace including nested observations, input/output, metadata, latency, and token usage. Requires `traceId`. |
| `find_exceptions` | Traces whose `level` equals `ERROR`. Supports time range and pagination. |
| `find_exceptions_in_file` | Error-level traces whose metadata contains a given file name substring. Requires `fileName`. |
| `get_exception_details` | Full detail of a single error trace. Requires `traceId`. |
| `get_error_count` | Count of `ERROR`-level traces in a time range (scans up to 500 traces). |
| `delete_trace` | Permanently deletes a single trace by ID. **Irreversible.** |
| `delete_traces` | Permanently deletes multiple traces. Pass a comma-separated list of trace IDs. **Irreversible.** |

---

### Sessions (3 tools)

| Tool | Description |
|---|---|
| `fetch_sessions` | Paginated list of sessions with optional time range filter. |
| `get_session_details` | Full session detail including all its traces. Requires `sessionId`. |
| `get_user_sessions` | All sessions for a specific user with pagination. Requires `userId`. |

---

### Prompts (5 tools)

| Tool | Description |
|---|---|
| `list_prompts` | Paginated list of all prompts in the project. |
| `get_prompt` | Fetch a prompt by name. Optionally pin to a `version` number or a `label` (e.g. `production`, `staging`). |
| `create_prompt` | Create a new prompt or append a new version to an existing prompt. `type` is `text` (plain string) or `chat` (JSON array of `{role, content}` messages). Supports comma-separated `labels` and `tags`. |
| `delete_prompt` | Delete prompt versions by name. Scope to a specific `label` or `version`; omit both to delete all versions. **Irreversible.** |
| `update_prompt_labels` | Replace the full label set on a specific prompt version. Supply an empty string to remove all labels. The `latest` label is reserved by Langfuse. |

---

### Datasets (7 tools)

| Tool | Description |
|---|---|
| `list_datasets` | Paginated list of all evaluation datasets. |
| `get_dataset` | Fetch a dataset by exact name. |
| `create_dataset` | Create a new dataset. Optionally supply `description`, `metadataJson`, `inputSchemaJson`, and `expectedOutputSchemaJson` (all as JSON strings). |
| `list_dataset_items` | Paginated list of items in a dataset. Requires `datasetName`. |
| `get_dataset_item` | Fetch a single dataset item by ID. |
| `create_dataset_item` | Create or upsert a dataset item. Optionally link to a `sourceTraceId` or `sourceObservationId`. Supports `itemId` for upsert semantics. |
| `delete_dataset_item` | Permanently delete a dataset item by ID. **Irreversible.** |

---

### Dataset Runs (5 tools)

| Tool | Description |
|---|---|
| `list_dataset_runs` | Paginated list of experiment runs for a dataset. Requires `datasetName`. |
| `get_dataset_run` | Full run detail including all run items. Requires `datasetName` and `runName`. |
| `delete_dataset_run` | Delete a run and all its items. **Irreversible.** Requires `datasetName` and `runName`. |
| `list_dataset_run_items` | Paginated list of items in a run. Requires `datasetId` and `runName`. |
| `create_dataset_run_item` | Create a run item linking a dataset item to a trace/observation. Creates the run automatically if it does not yet exist. |

---

### Metrics (1 tool)

| Tool | Description |
|---|---|
| `get_cost_metrics` | Query Langfuse cost, token, latency, and usage analytics via the Metrics API v1. Mirrors: GET /api/public/metrics?query=<json>. Pass the full query as a JSON string. All aggregation is server-side. |

This tool accepts a single required parameter `query` which must be a JSON-serialised string matching the Metrics API schema. Examples (pass these as a single JSON string):

- Total cost last 7 days:

  {"view":"traces","metrics":[{"measure":"totalCost","aggregation":"sum"}],"fromTimestamp":"2026-03-18T00:00:00Z","toTimestamp":"2026-03-25T23:59:59Z"}

- Daily cost trend this week:

  {"view":"traces","metrics":[{"measure":"totalCost","aggregation":"sum"},{"measure":"count","aggregation":"count"}],"timeDimension":{"granularity":"day"},"fromTimestamp":"2026-03-18T00:00:00Z","toTimestamp":"2026-03-25T23:59:59Z"}

- Cost by model:

  {"view":"observations","dimensions":[{"field":"providedModelName"}],"metrics":[{"measure":"totalCost","aggregation":"sum"},{"measure":"totalTokens","aggregation":"sum"}],"fromTimestamp":"2026-03-18T00:00:00Z","toTimestamp":"2026-03-25T23:59:59Z"}

- Cost for a specific user:

  {"view":"traces","metrics":[{"measure":"totalCost","aggregation":"sum"}],"filters":[{"column":"userId","operator":"=","value":"user-123","type":"string"}],"fromTimestamp":"2026-03-18T00:00:00Z","toTimestamp":"2026-03-25T23:59:59Z"}

- Production environment only:

  filters: [{"column":"environment","operator":"=","value":"production","type":"string"}]

---

### Scores (6 tools)

| Tool | Description |
|---|---|
| `get_scores` | Paginated list of evaluation scores. Filter by `traceId`, `observationId`, `name`, `dataType` (`NUMERIC`\|`CATEGORICAL`\|`BOOLEAN`), and time range. |
| `get_score` | Fetch a single score by ID. |
| `get_score_configs` | Paginated list of score config schemas. |
| `get_score_config` | Fetch a single score config by ID. |
| `create_score_config` | Create a score config. `NUMERIC` supports optional `minValue`/`maxValue`. `CATEGORICAL` accepts a `categoriesJson` array of `{label, value}` objects. |
| `update_score_config` | Update an existing score config. Optionally set `isArchived` to archive it. |

---

### Annotation Queues (8 tools)

| Tool | Description |
|---|---|
| `list_annotation_queues` | Paginated list of annotation queues. |
| `get_annotation_queue` | Fetch a single queue by ID. |
| `create_annotation_queue` | Create a queue for human-in-the-loop review. Optionally link a `scoreConfigId`. |
| `list_annotation_queue_items` | Paginated list of items in a queue. Optionally filter by `status` (`PENDING`\|`COMPLETED`). Requires `queueId`. |
| `get_annotation_queue_item` | Fetch a specific queue item by `queueId` and `itemId`. |
| `create_annotation_queue_item` | Add a trace, observation, or session to a queue for review. `objectType` is `TRACE`, `OBSERVATION`, or `SESSION`. |
| `update_annotation_queue_item` | Update the status of a queue item (`PENDING`\|`COMPLETED`). |
| `delete_annotation_queue_item` | Remove an item from a queue. **Irreversible.** |

---

### Comments (3 tools)

| Tool | Description |
|---|---|
| `get_comments` | Paginated list of comments. Optionally filter by `objectType` (`TRACE`\|`OBSERVATION`) and `objectId`. |
| `get_comment` | Fetch a single comment by ID. |
| `create_comment` | Attach a comment to a trace, observation, session, or prompt. `objectType` values: `TRACE`, `OBSERVATION`, `SESSION`, `PROMPT`. |

---

### Models (4 tools)

| Tool | Description |
|---|---|
| `list_models` | Paginated list of all model definitions (Langfuse-managed and custom). |
| `get_model` | Fetch a model definition by ID. |
| `create_model` | Create a custom model for cost tracking. Requires `modelName`, `matchPattern` (regex), and `unit` (`TOKENS`\|`CHARACTERS`\|`MILLISECONDS`\|`SECONDS`\|`IMAGES`\|`REQUESTS`). Optionally set per-unit USD prices. |
| `delete_model` | Delete a custom model definition. Langfuse-managed models cannot be deleted. **Irreversible.** |

---

### LLM Connections (2 tools)

| Tool | Description |
|---|---|
| `list_llm_connections` | Paginated list of LLM provider connections (secret keys are masked in the response). |
| `upsert_llm_connection` | Create or update a provider connection by `provider` name (e.g. `openai`, `anthropic`, `azure`, `google`). Upserts by provider — if a connection already exists it is updated. |

---

### Project (1 tool)

| Tool | Description |
|---|---|
| `get_projects_for_api_key` | Returns the project(s) visible to the configured API key. Useful for confirming credentials and project metadata. |

---

### Users (1 tool)

| Tool | Description |
|---|---|
| `get_user_traces` | All traces for a specific Langfuse user ID with pagination. Requires `userId`. |

---

### Schema (1 tool)

| Tool | Description |
|---|---|
| `get_data_schema` | Returns the full Langfuse data model: all entity types, fields, and valid enum values. Call this first to understand the available data structures before running queries. |

---

## Architecture

```
MCP Client (Cursor / Claude Desktop / Copilot / other)
    │   Streamable HTTP transport (/mcp)
    ▼
Tool class  (@McpTool — validates required params, delegates to service)
    ▼
Service interface + impl  (business logic, filtering, error mapping)
    ▼
LangfuseApiClient  (HTTP gateway — GET / POST / PATCH / DELETE, typed exceptions)
    ▼
Langfuse Public REST API
```

The architecture is strictly layered:

- **`client/`** — Langfuse integration boundary: HTTP with Basic-Auth (Apache HttpComponents 5), typed exceptions, `UriComponentsBuilder` for query params
- **`service/`** — domain logic: filtering, mapping, pagination, error translation into `ApiResponse`
- **`tools/`** — MCP surface: agent-friendly descriptions, parameter validation, delegation to services
- **Spring Boot** — runtime and transport wrapper only

### API Client

`LangfuseApiClient` supports four HTTP methods. All methods throw `LangfuseApiException` or `ResourceNotFoundException` on error, which the service layer converts into structured `ApiResponse.error(...)` responses — agents never see raw stack traces.

| Method | Used for |
|---|---|
| `GET` | All read operations |
| `POST` | Create operations |
| `PATCH` | Update operations |
| `DELETE` | Delete operations |

---

## Package Structure

```
com.langfuse.mcp
├── LangfuseMcpApplication.java          @SpringBootApplication @ConfigurationPropertiesScan
├── config/
│   ├── LangfuseProperties.java          @ConfigurationProperties — publicKey, secretKey, host, timeout, readOnly
│   ├── LangfuseClientConfig.java        RestClient bean — Basic-Auth, Apache HttpComponents 5, configurable timeout
│   └── JacksonConfig.java               Primary ObjectMapper (JSR310, ignore unknown fields)
├── client/
│   └── LangfuseApiClient.java           HTTP gateway (GET/POST/PATCH/DELETE); typed exceptions; UriComponentsBuilder queries
├── controller/
│   └── PingController.java              GET /ping → {"status":"ok"}
├── exception/
│   ├── LangfuseApiException.java        Wraps HTTP/connectivity errors — statusCode + endpoint
│   └── ResourceNotFoundException.java   Thrown on HTTP 404
├── dto/
│   ├── common/    ApiResponse · PagedResponse · PaginationMeta
│   ├── request/   Filter/get request classes (12 classes)
│   └── response/  Response classes (19 classes — JsonNode for open-schema fields)
├── service/       Interfaces (15): Trace · Session · Prompt · PromptWrite · Dataset · DatasetRun
│   │              · Score · AnnotationQueue · Comment · Model · LlmConnection · Project · User · Schema · CostMetrics
│   └── impl/      *ServiceImpl (15) — business logic, filtering, error mapping
├── tools/         @McpTool classes (15) — param validation, delegation, agent-friendly descriptions
│   ├── TraceTools.java             (8 tools)
│   ├── SessionTools.java           (3 tools)
│   ├── PromptTools.java            (2 tools)
│   ├── PromptWriteTools.java       (3 tools)
│   ├── DatasetTools.java           (7 tools)
│   ├── DatasetRunTools.java        (5 tools)
│   ├── ScoreTools.java             (6 tools)
│   ├── AnnotationQueueTools.java   (8 tools)
│   ├── CommentTools.java           (3 tools)
│   ├── ModelTools.java             (4 tools)
│   ├── LlmConnectionTools.java     (2 tools)
│   ├── ProjectTools.java           (1 tool)
│   ├── UserTools.java              (1 tool)
│   ├── SchemaTools.java            (1 tool)
│   └── CostMetricsTools.java       (1 tool)
└── util/
    └── JsonPageMapper.java         Centralised JSON → PagedResponse mapper (no duplication)
```

---

## Running Tests

```bash
mvn test
```

Test coverage includes:

- `LangfusePropertiesBindingTest` — config binding from `application-test.yml` and property-level validation
- `PromptWriteServiceImplTest` — service logic for prompt create / delete / label update
- `ProjectServiceImplTest` — project API response mapping
- `ObservationServiceImplTest` — observation fetch and field mapping
- `MetricsServiceImplTest` — metrics aggregation logic

Tests run with `spring.ai.mcp.server.enabled=false` (set in `src/test/resources/application-test.yml`) so no MCP transport is started during test execution.

---

## Troubleshooting

### `TRACE_FETCH_ERROR: HTTP/1.1 header parser received no bytes`

Connectivity issue — not a code bug. Check:

1. `LANGFUSE_HOST` points to a running Langfuse instance
2. The host is reachable from the JVM process
3. For Docker: use `host.docker.internal` instead of `localhost`
4. The scheme matches your server (`http://` vs `https://`)
5. Confirm the API is up: `curl $LANGFUSE_HOST/api/public/health`

### `INVALID_INPUT: <param> is required`

A required parameter was not provided. All `required = true` parameters are validated at the tool layer before any HTTP call is made.

### Connection timeouts

Increase the timeout:

```bash
export LANGFUSE_TIMEOUT=60s
```

### Agent cannot see the server

1. Confirm the server is running: `curl http://localhost:8080/actuator/health`
2. Confirm the MCP endpoint is reachable: `curl http://localhost:8080/ping`
3. Check that the client config URL points to `http://localhost:8080/mcp`
4. Inspect all available tools: `npx @modelcontextprotocol/inspector http://localhost:8080/mcp`

### Langfuse-managed models cannot be deleted

`delete_model` only works for custom model definitions you have created. To override a Langfuse-managed model's pricing, create a new custom model with the same `modelName`.

---

## License

[![License: MIT](https://img.shields.io/badge/License-MIT-yellow?style=flat&logo=opensourceinitiative&logoColor=white)](https://opensource.org/licenses/MIT)

TDQS

B3.2/5.0

Scored across 55 tools

Disambiguation4/5

Most tools are clearly distinct by resource and action, such as create_annotation_queue vs. get_annotation_queue. However, some overlap exists in read operations like fetch_trace and get_exception_details, which both retrieve trace details but with different filters, potentially causing confusion. The descriptions help clarify, but the sheer number of tools (55) increases the risk of misselection.

Naming Consistency5/5

Tool names follow a highly consistent verb_noun pattern throughout, such as create_annotation_queue, get_annotation_queue, list_annotation_queues, and update_annotation_queue_item. All tools use snake_case without deviation, making the naming predictable and easy to understand. This consistency aids in agent navigation and reduces cognitive load.

Tool Count2/5

With 55 tools, the count is excessive for an MCP server, far beyond the typical well-scoped range of 3-15 tools. This large number suggests over-fragmentation, such as having separate tools for fetch_trace and get_exception_details, which could be consolidated. It will likely overwhelm agents and increase complexity unnecessarily.

Completeness5/5

The tool set provides comprehensive CRUD and lifecycle coverage for the Langfuse domain, including traces, sessions, datasets, prompts, models, scores, annotation queues, and LLM connections. There are no obvious gaps; every major resource has create, read, update, and delete operations, along with specialized tools for analytics and filtering, ensuring agents can handle full workflows without dead ends.

Maintenance

ActivityInactive
ResponsivenessNo issues