Skip to main content
Glama
bfernandez2-seidor

ccv2-logs-mcp

README.md
# ccv2-logs-mcp

[![CI](https://github.com/bfernandez2-seidor/ccv2-logs-mcp/actions/workflows/ci.yml/badge.svg)](https://github.com/bfernandez2-seidor/ccv2-logs-mcp/actions/workflows/ci.yml)
[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
[![Node](https://img.shields.io/badge/node-%3E%3D20.19-brightgreen.svg)](https://nodejs.org)

Search SAP Commerce Cloud (CCv2) runtime logs from your AI agent, without leaving the conversation.

SAP does not expose a public API for CCv2 runtime logs — the only supported access is the SAP Cloud Logging
dashboard (OpenSearch Dashboards 2.x) in your browser. This MCP server reuses the SSO session cookie from that
browser to query the same backend the dashboard uses, and exposes three read-only tools: full-text search,
ERROR aggregation, and anchored context windows.

Read-only. No writes, no deletes, no configuration changes. Works with any CCv2 project.

## Quick path

1. `npm install && npm run build`
2. Export `CCV2_LOGS_HOST_<ENV>` for each environment you want to reach.
3. Extract a session cookie: `node scripts/extract-cookie.mjs s1`
4. Register the server with Claude Code and ask: *"any errors on s1 in the last hour?"*

Full detail for each step is below.

## How it works

```
  Chrome (dedicated profile, SSO done manually, --remote-debugging-port=9222)
       |
       |  scripts/extract-cookie.mjs <env>   (CDP Network.getCookies, 127.0.0.1 only)
       v
  .ccv2-session-<env>          file, mode 0600, gitignored
       |
       |  re-read on every request (refresh the file, no restart needed)
       v
  +-----------------------------------------------+
  |  ccv2-logs-mcp  (stdio MCP server)            |
  |                                               |
  |  tools/   MCP adapters, zod schemas           |
  |  query/   pure OpenSearch DSL builders        |
  |  parse/   pure response -> typed result       |
  |  config/  env -> host + session file          |
  |  client/  the only HTTP + fs boundary         |
  +-----------------------------------------------+
       |
       |  POST https://<host>/internal/search/opensearch-with-long-numerals
       |  Cookie: <session>   osd-xsrf: true
       v
  SAP Cloud Logging (OpenSearch Dashboards 2.x)  index pattern logs-json-*
```

The MCP client (Claude Code, or any stdio MCP host) talks to the server over stdio. There is no HTTP listener,
no daemon, and no credential storage beyond the cookie file you create yourself.

## Requirements

| Requirement | Why |
|-------------|-----|
| Node.js >= 20.19 | ESM, native `fetch`, and the toolchain the test suite is pinned to |
| A Chromium-based browser | Needed once per session to complete SSO (including 2FA) interactively |
| SAP Cloud Logging access | Your account must already be able to open the environment's Logging dashboard |

## Install

```bash
git clone https://github.com/bfernandez2-seidor/ccv2-logs-mcp.git
cd ccv2-logs-mcp
npm install
npm run build
```

The build emits `dist/index.js`, which is what the MCP client executes.

## Configuration

One environment variable per environment, named after the environment in uppercase:

```bash
CCV2_LOGS_HOST_S1=<dashboards host for s1>
CCV2_LOGS_HOST_P2=<dashboards host for p2>
CCV2_LOGS_HOST_D1=<dashboards host for d1>
```

**Where to find the host.** SAP Cloud Portal -> your environment -> *Logging*. Open the OpenSearch Dashboards
URL and copy the hostname only, without scheme or path (for example `dashboards-xxxxxxxx.cloud.logs.services.<region>.example`).

The environment name is free-form and case-insensitive: `CCV2_LOGS_HOST_STAGING` makes `env: "staging"` valid.
Any environment without a configured host is rejected with `unknown_env`, and the error lists the ones that
are configured.

| Variable | Required | Default | Purpose |
|----------|----------|---------|---------|
| `CCV2_LOGS_HOST_<ENV>` | yes, one per environment | — | Dashboards hostname for `<ENV>` |
| `CCV2_SESSION_DIR` | recommended | process working directory | Directory holding the `.ccv2-session-<env>` files |

> **Set `CCV2_SESSION_DIR` to the absolute path of the clone.** `scripts/extract-cookie.mjs` always writes the
> cookie to the package root, while the server looks for it relative to whatever working directory the MCP
> client happens to launch it in. Setting `CCV2_SESSION_DIR` explicitly removes the ambiguity.

## Session workflow

The cookie is short-lived — roughly **one hour** in practice. The flow is deliberately manual: SSO with 2FA
cannot be automated, and it should not be.

**First time, per working session:**

1. Launch a dedicated Chrome instance with remote debugging enabled and its own profile, so your everyday
   browser is never exposed on the debugging port:

   ```bash
   google-chrome \
     --remote-debugging-port=9222 \
     --user-data-dir="$HOME/.ccv2-logs-chrome"
   ```

2. In that window, open the environment's Logging endpoint from SAP Cloud Portal and complete SSO manually,
   including 2FA. Leave the tab open.

3. Extract the cookie into `.ccv2-session-<env>`:

   ```bash
   node scripts/extract-cookie.mjs s1
   ```

   The script finds the Cloud Logging tab over CDP, reads its cookies, and writes them to
   `.ccv2-session-s1` with mode `0600`. It prints the host it read them from, so you can confirm you extracted
   from the right environment.

**When the session expires** (tools start returning `expired_session`): switch back to that Chrome window,
reload the dashboard tab so SSO refreshes, and re-run the script. The server re-reads the file on every
request, so there is nothing to restart.

Repeat step 3 with a different `<env>` argument for each environment — one cookie file per environment,
extracted from a tab pointing at that environment.

## Register with Claude Code

```bash
claude mcp add --transport stdio --scope user ccv2-logs \
  --env CCV2_LOGS_HOST_S1=<host> \
  --env CCV2_SESSION_DIR=/absolute/path/to/ccv2-logs-mcp \
  -- /absolute/path/to/node20/bin/node /absolute/path/to/ccv2-logs-mcp/dist/index.js
```

Pass one `--env CCV2_LOGS_HOST_<ENV>=<host>` per environment. Three details that matter:

- **Use an absolute path to the Node binary**, not plain `node`. The registered command resolves through the
  MCP client's `PATH`, which may point at an older Node than the one you installed with (this package requires
  Node >= 20.19). Find yours with `command -v node` in the shell where `npm install` succeeded
  (e.g. `~/.nvm/versions/node/v20.19.6/bin/node`).
- **Use absolute paths everywhere else too** — the MCP client's working directory is not guaranteed to be the clone.
- **`--scope user` makes the server available in every project.** Without it, `claude mcp add` registers the
  server only for the directory you run it from.

A newly registered MCP server is not loaded into already-running sessions: restart Claude Code (or start a new
session), verify with `claude mcp list`, then ask the agent something like *"summarize errors on s1 for the
last 6 hours"*.

## Tools

All three tools take an `env` argument naming a configured environment. Time arguments accept ISO 8601
timestamps or OpenSearch date math (`now`, `now-1h`, `now-7d`). Results are returned as JSON text.

Messages longer than 500 characters are truncated with a ` …[truncated]` marker; the untruncated length is
always reported in `messageLength`, so you can tell a genuinely short message from a clipped one.

### `search_logs`

Full-text search over runtime logs, newest first.

| Parameter | Type | Default | Notes |
|-----------|------|---------|-------|
| `env` | string | — | Environment name, e.g. `s1` |
| `query` | string | — | Free text over message, `msg`, and logger name. Terms are ANDed |
| `aspect` | string | — | `accstorefront`, `backoffice`, `backgroundProcessing`, `api`. Case-insensitive |
| `level` | string | — | `ERROR`, `WARN`, `INFO`, `DEBUG`. Case-insensitive |
| `from` | string | `now-1h` | Start of range |
| `to` | string | `now` | End of range |
| `limit` | integer 1–500 | `50` | Lines to return |
| `offset` | integer >= 0 | `0` | Lines to skip. `offset + limit` must be <= 10000 |

```jsonc
// search_logs { env: "s1", query: "timeout", level: "ERROR", from: "now-2h" }
{
  "total": 128,
  "totalRelation": "eq",
  "returned": 2,
  "offset": 0,
  "logs": [
    {
      "timestamp": "2026-07-25T10:04:11.482Z",
      "level": "ERROR",
      "aspect": "accstorefront",
      "container": "accstorefront",
      "pod": "accstorefront-5f9c7d8b6c-h2xql",
      "logger": "com.example.checkout.PaymentServiceImpl",
      "thread": "http-nio-8080-exec-12",
      "message": "Payment gateway timeout after 30000 ms for order 000012345",
      "messageLength": 58
    },
    {
      "timestamp": "2026-07-25T10:03:58.117Z",
      "level": "ERROR",
      "aspect": "api",
      "container": "api",
      "pod": "api-6d4b9c7f55-q8n2v",
      "logger": "com.example.integration.OrderExportJob",
      "thread": "backgroundProcessing-worker-4",
      "message": "Read timeout calling downstream order service",
      "messageLength": 45
    }
  ]
}
```

`totalRelation` is `gte` when OpenSearch stopped counting past its tracking limit — treat `total` as a lower
bound in that case.

### `error_summary`

Aggregates ERROR-level lines by logger over a range. The `ERROR` filter is intrinsic to the tool and is not
parameterizable. Use it to find *what* is failing before drilling in with `search_logs`.

| Parameter | Type | Default | Notes |
|-----------|------|---------|-------|
| `env` | string | — | Environment name |
| `from` | string | `now-1h` | Start of range |
| `to` | string | `now` | End of range |
| `aspect` | string | — | Optional aspect filter |
| `size` | integer 1–100 | `20` | Maximum number of distinct loggers returned |

```jsonc
// error_summary { env: "s1", from: "now-6h" }
{
  "total": 342,
  "totalRelation": "eq",
  "groups": [
    {
      "logger": "com.example.checkout.PaymentServiceImpl",
      "count": 210,
      "aspects": [
        { "aspect": "accstorefront", "count": 190 },
        { "aspect": "api", "count": 20 }
      ],
      "sample": "Payment gateway timeout after 30000 ms for order 000012345"
    },
    {
      "logger": "com.example.integration.OrderExportJob",
      "count": 132,
      "aspects": [{ "aspect": "backgroundProcessing", "count": 132 }],
      "sample": "Read timeout calling downstream order service"
    }
  ]
}
```

An empty range is not an error: zero ERROR lines yields `groups: []`.

### `log_context`

Retrieves the log lines surrounding an anchor event, scoped to the same pod, merged into one chronological
window. This is how you turn "here is the exception" into "here is what the pod was doing around it".

| Parameter | Type | Default | Notes |
|-----------|------|---------|-------|
| `env` | string | — | Environment name |
| `timestamp` | string | — | Anchor timestamp, ISO 8601. Copy it from a `search_logs` result |
| `pod` | string | — | Pod name. Copy it from a `search_logs` result |
| `before` | integer 0–200 | `20` | Lines before the anchor |
| `after` | integer 0–200 | `20` | Lines after the anchor |

```jsonc
// log_context { env: "s1", timestamp: "2026-07-25T10:04:11.482Z", pod: "accstorefront-5f9c7d8b6c-h2xql", before: 2, after: 1 }
{
  "anchorIndex": 2,
  "logs": [
    { "timestamp": "2026-07-25T10:04:11.402Z", "level": "INFO",  "logger": "com.example.checkout.CartFacade", "message": "Placing order 000012345", "messageLength": 24, "pod": "accstorefront-5f9c7d8b6c-h2xql" },
    { "timestamp": "2026-07-25T10:04:11.451Z", "level": "DEBUG", "logger": "com.example.checkout.PaymentServiceImpl", "message": "Authorizing 149.90 EUR", "messageLength": 22, "pod": "accstorefront-5f9c7d8b6c-h2xql" },
    { "timestamp": "2026-07-25T10:04:11.482Z", "level": "ERROR", "logger": "com.example.checkout.PaymentServiceImpl", "message": "Payment gateway timeout after 30000 ms for order 000012345", "messageLength": 58, "pod": "accstorefront-5f9c7d8b6c-h2xql" },
    { "timestamp": "2026-07-25T10:04:11.503Z", "level": "WARN",  "logger": "com.example.checkout.CartFacade", "message": "Rolling back order 000012345", "messageLength": 28, "pod": "accstorefront-5f9c7d8b6c-h2xql" }
  ]
}
```

`anchorIndex` points at the anchor line inside `logs`.

**Context is positional, not identity-based.** Log lines carry no stable document identity here, so the anchor
is resolved by position: if several lines share the exact anchor instant, the first one encountered is treated
as the anchor and the rest fall into the before-window. The window is still correct and complete; only which
of the tied lines is labelled "the anchor" is arbitrary.

## Security model

This tool holds a live SSO session for a production-capable system. Treat it accordingly.

| Concern | Position |
|---------|----------|
| Cookies are secrets | They live only in `.ccv2-session-<env>` files, mode `0600`, matched by `.gitignore`. Never in source, never in test fixtures, never in logs or error messages |
| Cookies are never bundled | The `files` allowlist in `package.json` ships `dist`, `scripts`, `README.md`, `LICENSE` — nothing else |
| Debug port exposure | The extraction script only talks to `127.0.0.1:9222`. Launch Chrome with a dedicated `--user-data-dir` so the debugging port never exposes your everyday profile |
| Blast radius | The server issues read-only search requests. It cannot write, delete, or reconfigure anything |
| PII in production logs | Production runtime logs can contain customer data. Prefer staging environments for exploratory work, and keep production queries narrow and purposeful |
| Transport | stdio only. No network listener is opened by this server |

### Known advisory

`npm audit` reports a moderate advisory for `@hono/node-server` `<2.0.5` (path traversal in `serve-static` on
Windows), reached transitively through `@modelcontextprotocol/sdk`.

**Not applicable to this server.** `ccv2-logs-mcp` is stdio-only: it imports `StdioServerTransport` and never
loads the SDK's HTTP transport, so `serve-static` is never on any code path here. The advisory is tracked
upstream in the SDK and will clear when the SDK bumps its dependency.

## Troubleshooting

Every failure is a typed error carrying an actionable remediation. The message tells you what to do.

| Symptom | Kind | Fix |
|---------|------|-----|
| `Unknown environment "x". Configured: s1.` | `unknown_env` | Set `CCV2_LOGS_HOST_X`. The message lists what *is* configured — a typo in the env name is the usual cause |
| `No session file at ...` | `missing_session` | Run `node scripts/extract-cookie.mjs <env>`. If the file exists but is not found, `CCV2_SESSION_DIR` is not pointing at the clone |
| `Session for "s1" looks expired (HTTP 401)` | `expired_session` | Reload the dashboard tab in the dedicated Chrome window to refresh SSO, re-run the extraction script. No restart needed — the cookie is re-read per request. Also raised when the upstream returns an HTML login page with a 200 status |
| `offset (9990) + limit (50) exceeds the maximum result window (10000)` | `invalid_argument` | Deep pagination is capped by the index's `max_result_window`. Narrow the time range or add a `query`/`aspect` filter instead of paging further |
| `No document found for timestamp "..." on pod "..."` | `anchor_not_found` | The anchor must exist exactly as given. Copy `timestamp` and `pod` verbatim from a `search_logs` result; a rounded timestamp or a recycled pod name will not match |
| `Unexpected response shape: ...` | `unexpected_response` | Usually a stale cookie or a changed upstream contract. Re-extract the cookie first |
| `Upstream request failed (HTTP 5xx)` | `upstream_http` | Transient upstream problem, or a payload too large. Retry, then narrow the query |

**`extract-cookie.mjs` fails.**

- *"Cannot reach Chrome CDP"* — Chrome is not running with `--remote-debugging-port=9222`, or it was already
  running under a different profile and reused that instance. Quit Chrome fully and relaunch with the flag.
- *"No Cloud Logging tab open"* — the dashboard tab was closed or navigated away. Reopen the environment's
  Logging endpoint.
- *"No cookies for ..."* — SSO did not complete. Finish the login (including 2FA) in that window first.

## Development

```bash
npm test          # vitest run, 77 unit tests
npm run test:watch
npm run build     # tsc -> dist/
```

The suite is fully offline: fetch and filesystem access are injected through `ClientDeps`, and every fixture
is synthetic.

**Live end-to-end suite.** `test/live/s1.e2e.test.ts` exercises the real dashboard and is skipped unless
explicitly enabled. It needs a valid cookie for the target environment:

```bash
CCV2_LIVE_E2E=1 CCV2_LOGS_HOST_S1=<host> npx vitest run test/live/s1.e2e.test.ts
```

### Architecture

Ports and adapters. The dependency direction is inward, and everything except `client/` is pure.

| Layer | Files | Responsibility |
|-------|-------|----------------|
| `src/index.ts` | 1 | Bootstrap only: build client, register tools, connect stdio |
| `src/tools/` | 5 | MCP adapters — zod schemas, MCP result mapping. No log-domain logic |
| `src/query/` | 4 | Pure OpenSearch DSL builders and field constants |
| `src/parse/` | 4 | Pure response parsing — envelope unwrapping, hits, aggregations, context merge |
| `src/config/` | 1 | `env` -> host + session file path, pure over an injected `ProcessEnv` |
| `src/client/` | 2 | The only I/O in the codebase: one `fetch`, one `readFileSync` |
| `src/errors.ts` | 1 | Typed error taxonomy, one factory per kind |

Two rules keep this honest, and both are worth preserving in contributions:

1. **I/O lives only in `client/`.** If a new feature needs the network or the filesystem, it goes through
   `LogsClient` or gets its own injectable boundary — it does not get added to `query/` or `parse/`.
2. **Every error is a `CcvLogsError` with a remediation.** An error the reader cannot act on is a bug.

See [CONTRIBUTING.md](CONTRIBUTING.md) for conventions and PR expectations.

## License

[MIT](LICENSE) © 2026 Benja Fernández

TDQS

A3.9/5.0

Scored across 1 tool

Disambiguation5/5

With only one tool, there is no ambiguity. The agent will always select search_logs for any log-related task.

Naming Consistency5/5

Single tool follows a clear verb_noun pattern (search_logs), consistent with MCP conventions.

Tool Count3/5

A single tool for log searching is borderline. While it covers the core function, a typical logging server might benefit from additional tools for listing sources or filtering by context.

Completeness3/5

The tool provides basic log search but lacks obvious features like log source enumeration, time-range filtering beyond what is implicit, or multi-aspect queries. This may cause agent failures for complex log retrieval tasks.

Maintenance

ActivityMaintained
ResponsivenessSyncing