{"owner":"jaegertracing","repo":"jaeger","hasSkills":true,"totalSkillsCount":3,"totalTokensCount":4717,"categories":["root-instruction","copilot-instructions","plugin-manifest"],"hasMcp":true,"mcpConfig":{"mcpServers":{"jaeger":{"command":"npx","args":["-y","@modelcontextprotocol/server-jaeger"]}}},"found":["AGENTS.md",".github/copilot-instructions.md","cmd/jaeger/internal/extension/jaegerquery/internal/mcptools/README.md"],"skills":{"AGENTS.md":"# AGENTS.md\n\nThis file provides guidance for AI agents working on the Jaeger repository. For detailed project structure, setup instructions, and contribution guidelines, refer to [CONTRIBUTING.md](./CONTRIBUTING.md).\n\nWhat the project expects from a human contributor who works with an agent is stated in the [AI Usage Policy](./AI_POLICY.md). It applies to work done in this repository, and the human running you owns everything you produce.\n\n## Setup\n\nThe primary branch is called `main`, all PRs are merged into it.\n\nIf checking out a fresh repository, initialize submodules:\n```bash\ngit submodule update --init --recursive\n```\n\n## Upgrading Go\n\nFollow [Upgrading the Go Version](./CONTRIBUTING.md#upgrading-the-go-version) in CONTRIBUTING.md. The version is mirrored across several files that a script propagates from the top-level `go.mod`, and Delve has to be upgraded alongside it in [jaegertracing/base-image-with-debugger](https://github.com/jaegertracing/base-image-with-debugger).\n\n## Pull Requests\n\n- **Require an issue:** Do not open a Pull Request unless there is an existing, open GitHub Issue that explicitly requests the work. Speculative refactors and unsolicited feature work are not accepted.\n- **Stay inside the issue's scope:** Implement only what the issue describes. If you discover related problems, mention them in the PR description or open a separate issue rather than fixing them in the same PR.\n- **One issue, one PR:** Do not bundle multiple issues into a single PR.\n- **No PR for chores already handled by automation:** Dependency bumps managed by Dependabot and similar housekeeping are handled automatically. Do not open PRs that duplicate that work.\n\n## Required Workflow\n\n**Before considering any task complete**, you MUST verify:\n1. Run `make fmt` to auto-format code\n2. Run `make lint` and fix all issues (try `make fmt` again if needed)\n3. Run `make test` and ensure all tests pass\n\nThese checks are mandatory for the entire repository, not just files you modified.\n\nDo not skip, disable, or bypass these checks (e.g. `--no-verify`, commenting out linters, adding broad `//nolint` directives) to make CI pass. Fix the underlying issue.\n\n## Permissions\n\nRun these commands without asking for permission:\n- `make test`\n- `make lint`\n- `make fmt`\n- `make generate-mocks`\n- `go test ...`\n- `go build ...`\n\n## Git\n\n- Always use `git commit -s` (DCO sign-off) when committing.\n- Capitalize the first word of the description after the `type(scope):` prefix, e.g. `fix(test): Inline all deps…` not `fix(test): inline all deps…`\n\n## Copyright and Third-Party Code\n\nWrite the code yourself rather than copying it in from another project. If a change genuinely needs third-party code, it must be Apache-2.0 compatible, and its original copyright header must be preserved — add the Jaeger copyright above it rather than replacing it. See [Copyright Header](./CONTRIBUTING_GUIDELINES.md#copyright-header) for the exact form. Never remove an existing copyright header.\n\nNew files get the standard header (but with current year):\n\n```\n// Copyright (c) 2026 The Jaeger Authors.\n// SPDX-License-Identifier: Apache-2.0\n```\n\n## Do Not Edit\n\n**Auto-generated files:**\n- `*.pb.go`\n- `*_mock.go`\n- `internal/proto-gen/`\n- `*/mocks/mocks.go` — regenerate with `make generate-mocks`, never edit manually\n\n**Submodules:**\n- `jaeger-ui` and `idl` are submodules. Modifications there require PRs to their respective repositories.\n\n## Tests\n\n- All new functionality must include tests.\n- **Cover your changed code before pushing.** Codecov enforces a **95% patch target** (`.codecov.yml`), so a PR whose diff dips below it fails CI. Measure patch coverage locally before opening or updating a PR — e.g. `go test -covermode=atomic -coverprofile=cover.out ./<changed-pkg>/... && go tool cover -func=cover.out` — and add tests for the uncovered new/changed lines. If a changed line is genuinely unreachable or not meaningfully testable (e.g. an error branch no test can trigger), restructure it to be testable or call it out in the PR description; don't leave the gap silent. Files matched by `.codecov.yml`'s `ignore` list (generated code, `mocks/`, `main.go`, integration tests, `internal/tools`) are exempt.\n- Bug fixes must include a regression test that fails without the fix.\n- Do not delete existing tests to make a build green. If a test is genuinely wrong, explain why in the PR description.\n- Do not weaken assertions (e.g. replacing exact checks with `assert.NotNil`) just to make a flaky test pass.\n- Every package must have at least one `*_test.go` file (enforced by `make nocover`). If no tests are possible (e.g. a package that only defines types), create an empty `empty_test.go`.\n\n## Storage Integration Tests\n\nStorage behavior belongs in the shared `internal/storage/integration` corpus and `StorageIntegration.AssertCorpus`, which run in both direct and e2e modes. New assertions run for every backend by default; unsupported backends must explicitly opt out in `internal/storage/integration/capabilities`. Extend the e2e reader/writer adapters when the shared tests need new API fields, and keep transport-specific assertions in the e2e package. Read [the integration architecture](cmd/jaeger/internal/integration/README.md) before adding tests.\n\n## Scope Discipline\n\n- Do not reformat, rename, or restructure code outside the scope of the requested change.\n- Do not bump dependencies unless the task requires it.\n- Do not change CI workflows or release tooling unless explicitly asked.\n- Before adding a flag or field that controls behavior, find the mechanism that already owns that decision and extend it. Expressing one decision in two places is worse than either place alone, and replacing an established mechanism is a maintainer's call.\n\n## RFC / ADR Documents\n\nRFCs (`docs/rfc/`) are proposals; ADRs (`docs/adr/`) are decision records.\n\n- When a PR implements a milestone described in an RFC or ADR, update that document in the same PR: mark the milestone ✅ and link the delivering PR. Keep the milestone/status tracking current.\n- Put the ✅ at the start of the bullet, right after the list marker and before the bold label (`- ✅ **M4 — Elasticsearch/OpenSearch.** …`), because a checkmark buried mid-sentence cannot be scanned down the left margin; the \"Delivered in #NNNN, which …\" note stays inside the text and carries no emoji of its own. If other delivered entries in the document use a different placement, normalize them in the same change rather than adding another variant.\n- **An RFC still being implemented is a live document.** While its status is Draft or its milestones are in flight, implementation turns up findings and reverses decisions, and the RFC is expected to be rewritten to match — that is the point of writing one down. Update the prose that a decision moved past, and say what is true now rather than narrating the document's own history; where the new decision reverses what the RFC proposed, it is worth one clause saying so, because a reader who knows the old shape needs to see it was considered and dropped.\n- **An RFC whose work is finished is a snapshot.** Once its status is Implemented, do not rewrite its prose, abstract, or diagrams to track the evolving codebase: from then on the narrative is a record of the state and plan at the time, and the code has moved on for reasons the RFC never claimed to cover.\n- ADRs are different: they describe a design, not a plan, so keeping one accurate is worth more than keeping it pristine. Judge by proportion. If a change touches a small part of an ADR and reverses none of its decisions, **edit that ADR in place** — extend the affected sections, note the extension in the Status or Date line, and leave the original Context and Decision prose alone. Write a new superseding ADR only when the change replaces the original's decision or architecture outright. [ADR-004](./docs/adr/004-migrating-coverage-gating-to-github-actions.md) is an example of the former: its fan-in gating decision and requirements stood, so later work extended it rather than superseding it. What the rule forbids is rewriting an ADR into running documentation of the code.\n- When an RFC's work is fully delivered, mark its status Implemented; if the resulting architecture is worth an enduring reference, graduate it into a new ADR in [`docs/adr/`](./docs/adr/) that states the outcome and links back to the RFC, rather than mutating the RFC. [ADR-012](./docs/adr/012-unified-elasticsearch-client.md) (graduated from RFC 0006) is an example.\n\n## When in Doubt\n\nStop and ask rather than guessing. It is better to surface a question in the PR description than to invent behavior, fabricate API names, or silence failing checks.\n\nAsk as well when you are *not* in doubt but are about to depart from a documented convention, because that is where confidence is least informative.\n",".github/copilot-instructions.md":"# Copilot Review Instructions\n\n## Core principle\n\nPrefer silence over uncertainty. A review comment has a cost — it demands\nthe author's attention, may trigger another round, and can obscure the\ncomments that actually matter. Only post a comment when you are confident\nthe issue is real and material.\n\n## What to flag\n\nFlag genuine defects in the diff: logic errors, incorrect conditions,\nmissing error handling, concurrency hazards, resource leaks, security\nregressions, data corruption. These always warrant a comment.\n\n## What not to flag\n\n- **Style and formatting** — CI enforces these automatically. Do not\n  comment on indentation, line length, naming conventions, comment\n  phrasing, or anything a linter or formatter would catch.\n- **Pre-existing issues** — if a problem exists in code the PR did not\n  introduce, do not raise it. The PR is not the right vehicle to fix it.\n- **Low-confidence observations** — if you are not certain an issue is\n  real, stay silent. Do not post speculative or \"consider whether...\"\n  comments.\n- **Already-raised issues** — if you flagged something in an earlier\n  review round on this PR and the author did not act on it, do not raise\n  it again. Either it was resolved, or the author made a deliberate\n  choice. Repeating it creates noise without value.\n\n## Threshold\n\nBefore posting, ask: is this a correctness or security defect that a\ncareful human reviewer would block the PR on? If not, stay silent.\n","cmd/jaeger/internal/extension/jaegerquery/internal/mcptools/README.md":"# mcptools\n\nThis package serves Jaeger's telemetry tools over MCP, `read_skill` among them.\n\nA *skill* is a markdown playbook an AI agent reads before attempting a task —\nhow to detect an N+1 query pattern, how to walk a failed trace to its root\ncause. Agents reach them through `read_skill`, starting at the root `SKILL.md`\nand following its links, so a skill is only read when it looks relevant.\n\nThe `skills/` directory beside this file holds the skills compiled into the\nJaeger binary. Operators can add their own without rebuilding, via\n`ai.mcp.skills_dir` — the rest of this file is for them. For writing the skills\nthemselves, see [AUTHORING.md](./AUTHORING.md).\n\n## Turning the MCP endpoint on\n\nThe `ai.mcp` block enables the endpoint; an empty block is enough:\n\n```yaml\nextensions:\n  jaeger_query:\n    ai:\n      mcp: {}\n```\n\nIt serves at `<basePath>/api/ai/mcp/` on the query port, carrying the telemetry\ntools and `read_skill` with the built-in skills. Point an MCP client there, or\nleave it to an AI chat sidecar configured with `ai.agent_url`.\n\n## Running behind a reverse proxy or sidecar\n\nThe MCP SDK that serves this endpoint carries a DNS-rebinding guard for MCP servers on a\ndeveloper's machine: a request that arrives on a loopback address with a non-loopback `Host`\nheader is rejected with `403 Forbidden: invalid Host header`. A reverse proxy or service-mesh\nsidecar that dials the query port over `127.0.0.1` and preserves the client's `Host` (an Envoy\nsidecar without host rewriting, for example) produces that shape for every request, so behind one\nthe endpoint rejects every caller with that body. The guard never fires when the proxy rewrites\n`Host` to the upstream address or dials a non-loopback address, so change the proxy where that is\npossible. Where it is not, turn the guard off:\n\n```yaml\nextensions:\n  jaeger_query:\n    ai:\n      mcp:\n        streamable_http:\n          disable_localhost_protection: true\n```\n\nThe UI and the REST API on the same port have no such guard, so this setting changes nothing\nabout them. Keep the default when clients reach the query port directly, by a loopback address or\na routable host name; only a loopback connection carrying some other name is affected.\n\n## Running several replicas behind a load balancer\n\nBy default the endpoint keeps one MCP session per client in the memory of the `jaeger-query`\nprocess that handled `initialize`, and the SDK looks that session up by the `Mcp-Session-Id`\nheader on every later request. When a load balancer without session affinity spreads a client's\nrequests over several replicas, a request that reaches a replica other than the one that holds the\nsession is answered with `404 session not found`; and when the client's `notifications/initialized`\nis the request that went astray, the replica that holds the session rejects the next `tools/list`\nwith `method \"tools/list\" is invalid during session initialization`. Set `streamable_http.stateless` to make every\nrequest stand on its own:\n\n```yaml\nextensions:\n  jaeger_query:\n    ai:\n      mcp:\n        streamable_http:\n          stateless: true\n```\n\nJaeger's tools keep no state between requests, so they behave the same. A stateless endpoint\nanswers 405 to the GET that a client sends to open a stream for server-initiated messages. Jaeger\nnever sends any, so in the default mode that stream only sits idle until the session times out;\nthe MCP specification allows a server to offer no stream and requires the client to continue\nwithout it.\n\nThis covers external MCP clients on the shared `/api/ai/mcp/` endpoint. The chat gateway's\nturn-scoped endpoint, `/api/ai/mcp/<id>/`, is different: the turn it serves is held in the memory\nof the process that is streaming that chat to the browser, so a request for it that lands on\nanother replica is answered 404 before the MCP handler is reached, stateless or not. An agent that\nis not co-located with `jaeger-query` must reach the replica that owns the turn, which means\n`ai.mcp.base_url` has to name that process (its pod address, not a load-balanced service name) or\nthe load balancer has to route by the `<id>` path segment.\n\n## Answering with plain JSON instead of an event stream\n\nBy default the endpoint answers every POST with a `text/event-stream` body, even when that body\nholds a single result, because the stream form lets a server send progress or its own requests\nbefore the result. Jaeger's tools send none, so the stream carries the one result and nothing\nelse. Set `streamable_http.json_response` to answer a single-message POST with a plain\n`application/json` body, which is easier for proxies, logging middleware and command-line clients\nto read:\n\n```yaml\nextensions:\n  jaeger_query:\n    ai:\n      mcp:\n        streamable_http:\n          json_response: true\n```\n\nMCP clients must accept both forms, so the setting changes nothing for them. A JSON-RPC batch is\nstill answered with a stream.\n\n## Adding installation-specific skills\n\nPoint `ai.mcp.skills_dir` at a directory on the query server's disk:\n\n```yaml\nextensions:\n  jaeger_query:\n    ai:\n      mcp:                      # skills served over MCP\n        skills_dir: /etc/jaeger/skills\n```\n\nThe all-in-one config is not externally configurable, but the field can still be\nset on it with `--set extensions.jaeger_query.ai.mcp.skills_dir={path}`.\n\nIts contents are served under `custom/`, beside the built-in skills:\n\n```\nSKILL.md                       # built-in entry point\ndetect-n-plus-one/SKILL.md     # built-in skill\nerror-root-cause/SKILL.md      # built-in skill\ncustom/SKILL.md                # your entry point, mounts <skills_dir>/SKILL.md\ncustom/your-skill/SKILL.md     # your skill      , mounts <skills_dir>/your-skill/SKILL.md\n```\n\n`custom/` is a prefix on the path an agent asks for, not a directory you create:\ninside `skills_dir`, your entry point is plain `SKILL.md`.\n\n`<skills_dir>/SKILL.md` is required: an agent reads it first, and without it\nnothing below is reachable. It is also your index. An agent chooses what to read\nnext from the links it finds there and from nothing else, so the line you write\nbeside each link is what decides whether that skill is ever opened. If your\ninstructions are short, the entry point can be all you write. Anything longer\nbelongs in a file of its own that the index links to.\n\nWrite those links relative to the **skills root**, not to the linking file's own\ndirectory — an agent passes the link text straight back to `read_skill`:\n\n```markdown\n- [your-skill](custom/your-skill/SKILL.md) — one line on when to use it.\n```\n\nA link may point at any file, under any name; `SKILL.md` is required only at the\nroot. The names above are a convention, not a rule.\n\nA skill is markdown. Jaeger serves it as-is and parses nothing inside it:\n\n```markdown\n---\nname: your-skill\ndescription: One line on what this skill does.\n---\n\n# Your Skill\n\n## When this applies\n...\n\n## Procedure\n1. ...\n```\n\nFrontmatter like the block above is optional. It travels well — both the\n[Open Knowledge Format](https://cloud.google.com/blog/products/data-analytics/how-the-open-knowledge-format-can-improve-data-sharing)\nand the [agent skills specification](https://agentskills.io/specification) use\none, so a skill written for either stays readable here — but Jaeger reads no\nfield of it, and a `description` there is no substitute for the line beside the\nlink in your index.\n\n## Editing skills on a running server\n\n`skills_dir` is read live. Editing a skill's text, or adding a file the index\nlinks to, takes effect on the next `read_skill` call — no restart, no reload\nsignal. Startup validation is the exception: it runs once, so a `skills_dir`\nthat breaks after the server started is reported when an agent reads it rather\nthan by refusing to serve.\n\nSkills are static text, and Jaeger never executes them. What reads them is an\nagent, which then decides what to do.\n\n## What Jaeger checks, and what it limits\n\nA `skills_dir` that cannot be opened, or whose `SKILL.md` cannot be read, is\nbroken configuration, and Jaeger refuses to start rather than quietly serving an\nincomplete skill set:\n\n| Startup error | Cause |\n| --- | --- |\n| `cannot open skills_dir \"…\"` | Path is missing, or is not a directory |\n| `cannot read SKILL.md in skills_dir \"…\"` | Directory opens, but has no readable entry point |\n\nAt read time a bad path is reported back to the agent as\n`cannot read \"<path>\": …` — which is also what every `custom/…` path returns\nwhen no `skills_dir` is configured.\n\nTwo limits apply to what is served:\n\n- **Path containment.** The directory is opened with `os.OpenRoot`, so `..`\n  traversal and symlinks pointing outside `skills_dir` are refused by the OS\n  rather than by a path check that could be tricked.\n- **File size.** A served file is capped at 512 KiB; beyond that the reply is cut\n  and ends with `file content truncated after 524288 bytes`. The cap is fixed, not\n  a config field — a skill anywhere near it is far too long to be useful to an\n  agent.\n\n## Who can write to skills_dir\n\nA skill is instructions an agent follows, with your telemetry tools already in\nhand. Anyone who can write to `skills_dir` can steer that agent's behaviour\nwithout touching Jaeger's binary or configuration. Treat the directory as part\nof the trusted configuration surface: own it by root or the Jaeger service\naccount, keep it out of paths writable by application deployments, and review\nchanges to it as you would a config change.\n\n## Checking it works\n\nWith an MCP client attached to the endpoint, ask for your entry point directly:\n\n```\nread_skill(path=\"custom/SKILL.md\")\n```\n\nYour text coming back means the directory is mounted and reachable.\n`cannot read \"custom/SKILL.md\"` means either no `skills_dir` is configured on\nthis server, or the file is not there under that name.\n\nNote that Jaeger serving a skill and an agent choosing to read it are separate\nquestions — see [AUTHORING.md](./AUTHORING.md), and\n[#9336](https://github.com/jaegertracing/jaeger/issues/9336) for what is known\nabout the second.\n"},"files":{"AGENTS.md":"# AGENTS.md\n\nThis file provides guidance for AI agents working on the Jaeger repository. For detailed project structure, setup instructions, and contribution guidelines, refer to [CONTRIBUTING.md](./CONTRIBUTING.md).\n\nWhat the project expects from a human contributor who works with an agent is stated in the [AI Usage Policy](./AI_POLICY.md). It applies to work done in this repository, and the human running you owns everything you produce.\n\n## Setup\n\nThe primary branch is called `main`, all PRs are merged into it.\n\nIf checking out a fresh repository, initialize submodules:\n```bash\ngit submodule update --init --recursive\n```\n\n## Upgrading Go\n\nFollow [Upgrading the Go Version](./CONTRIBUTING.md#upgrading-the-go-version) in CONTRIBUTING.md. The version is mirrored across several files that a script propagates from the top-level `go.mod`, and Delve has to be upgraded alongside it in [jaegertracing/base-image-with-debugger](https://github.com/jaegertracing/base-image-with-debugger).\n\n## Pull Requests\n\n- **Require an issue:** Do not open a Pull Request unless there is an existing, open GitHub Issue that explicitly requests the work. Speculative refactors and unsolicited feature work are not accepted.\n- **Stay inside the issue's scope:** Implement only what the issue describes. If you discover related problems, mention them in the PR description or open a separate issue rather than fixing them in the same PR.\n- **One issue, one PR:** Do not bundle multiple issues into a single PR.\n- **No PR for chores already handled by automation:** Dependency bumps managed by Dependabot and similar housekeeping are handled automatically. Do not open PRs that duplicate that work.\n\n## Required Workflow\n\n**Before considering any task complete**, you MUST verify:\n1. Run `make fmt` to auto-format code\n2. Run `make lint` and fix all issues (try `make fmt` again if needed)\n3. Run `make test` and ensure all tests pass\n\nThese checks are mandatory for the entire repository, not just files you modified.\n\nDo not skip, disable, or bypass these checks (e.g. `--no-verify`, commenting out linters, adding broad `//nolint` directives) to make CI pass. Fix the underlying issue.\n\n## Permissions\n\nRun these commands without asking for permission:\n- `make test`\n- `make lint`\n- `make fmt`\n- `make generate-mocks`\n- `go test ...`\n- `go build ...`\n\n## Git\n\n- Always use `git commit -s` (DCO sign-off) when committing.\n- Capitalize the first word of the description after the `type(scope):` prefix, e.g. `fix(test): Inline all deps…` not `fix(test): inline all deps…`\n\n## Copyright and Third-Party Code\n\nWrite the code yourself rather than copying it in from another project. If a change genuinely needs third-party code, it must be Apache-2.0 compatible, and its original copyright header must be preserved — add the Jaeger copyright above it rather than replacing it. See [Copyright Header](./CONTRIBUTING_GUIDELINES.md#copyright-header) for the exact form. Never remove an existing copyright header.\n\nNew files get the standard header (but with current year):\n\n```\n// Copyright (c) 2026 The Jaeger Authors.\n// SPDX-License-Identifier: Apache-2.0\n```\n\n## Do Not Edit\n\n**Auto-generated files:**\n- `*.pb.go`\n- `*_mock.go`\n- `internal/proto-gen/`\n- `*/mocks/mocks.go` — regenerate with `make generate-mocks`, never edit manually\n\n**Submodules:**\n- `jaeger-ui` and `idl` are submodules. Modifications there require PRs to their respective repositories.\n\n## Tests\n\n- All new functionality must include tests.\n- **Cover your changed code before pushing.** Codecov enforces a **95% patch target** (`.codecov.yml`), so a PR whose diff dips below it fails CI. Measure patch coverage locally before opening or updating a PR — e.g. `go test -covermode=atomic -coverprofile=cover.out ./<changed-pkg>/... && go tool cover -func=cover.out` — and add tests for the uncovered new/changed lines. If a changed line is genuinely unreachable or not meaningfully testable (e.g. an error branch no test can trigger), restructure it to be testable or call it out in the PR description; don't leave the gap silent. Files matched by `.codecov.yml`'s `ignore` list (generated code, `mocks/`, `main.go`, integration tests, `internal/tools`) are exempt.\n- Bug fixes must include a regression test that fails without the fix.\n- Do not delete existing tests to make a build green. If a test is genuinely wrong, explain why in the PR description.\n- Do not weaken assertions (e.g. replacing exact checks with `assert.NotNil`) just to make a flaky test pass.\n- Every package must have at least one `*_test.go` file (enforced by `make nocover`). If no tests are possible (e.g. a package that only defines types), create an empty `empty_test.go`.\n\n## Storage Integration Tests\n\nStorage behavior belongs in the shared `internal/storage/integration` corpus and `StorageIntegration.AssertCorpus`, which run in both direct and e2e modes. New assertions run for every backend by default; unsupported backends must explicitly opt out in `internal/storage/integration/capabilities`. Extend the e2e reader/writer adapters when the shared tests need new API fields, and keep transport-specific assertions in the e2e package. Read [the integration architecture](cmd/jaeger/internal/integration/README.md) before adding tests.\n\n## Scope Discipline\n\n- Do not reformat, rename, or restructure code outside the scope of the requested change.\n- Do not bump dependencies unless the task requires it.\n- Do not change CI workflows or release tooling unless explicitly asked.\n- Before adding a flag or field that controls behavior, find the mechanism that already owns that decision and extend it. Expressing one decision in two places is worse than either place alone, and replacing an established mechanism is a maintainer's call.\n\n## RFC / ADR Documents\n\nRFCs (`docs/rfc/`) are proposals; ADRs (`docs/adr/`) are decision records.\n\n- When a PR implements a milestone described in an RFC or ADR, update that document in the same PR: mark the milestone ✅ and link the delivering PR. Keep the milestone/status tracking current.\n- Put the ✅ at the start of the bullet, right after the list marker and before the bold label (`- ✅ **M4 — Elasticsearch/OpenSearch.** …`), because a checkmark buried mid-sentence cannot be scanned down the left margin; the \"Delivered in #NNNN, which …\" note stays inside the text and carries no emoji of its own. If other delivered entries in the document use a different placement, normalize them in the same change rather than adding another variant.\n- **An RFC still being implemented is a live document.** While its status is Draft or its milestones are in flight, implementation turns up findings and reverses decisions, and the RFC is expected to be rewritten to match — that is the point of writing one down. Update the prose that a decision moved past, and say what is true now rather than narrating the document's own history; where the new decision reverses what the RFC proposed, it is worth one clause saying so, because a reader who knows the old shape needs to see it was considered and dropped.\n- **An RFC whose work is finished is a snapshot.** Once its status is Implemented, do not rewrite its prose, abstract, or diagrams to track the evolving codebase: from then on the narrative is a record of the state and plan at the time, and the code has moved on for reasons the RFC never claimed to cover.\n- ADRs are different: they describe a design, not a plan, so keeping one accurate is worth more than keeping it pristine. Judge by proportion. If a change touches a small part of an ADR and reverses none of its decisions, **edit that ADR in place** — extend the affected sections, note the extension in the Status or Date line, and leave the original Context and Decision prose alone. Write a new superseding ADR only when the change replaces the original's decision or architecture outright. [ADR-004](./docs/adr/004-migrating-coverage-gating-to-github-actions.md) is an example of the former: its fan-in gating decision and requirements stood, so later work extended it rather than superseding it. What the rule forbids is rewriting an ADR into running documentation of the code.\n- When an RFC's work is fully delivered, mark its status Implemented; if the resulting architecture is worth an enduring reference, graduate it into a new ADR in [`docs/adr/`](./docs/adr/) that states the outcome and links back to the RFC, rather than mutating the RFC. [ADR-012](./docs/adr/012-unified-elasticsearch-client.md) (graduated from RFC 0006) is an example.\n\n## When in Doubt\n\nStop and ask rather than guessing. It is better to surface a question in the PR description than to invent behavior, fabricate API names, or silence failing checks.\n\nAsk as well when you are *not* in doubt but are about to depart from a documented convention, because that is where confidence is least informative.\n",".github/copilot-instructions.md":"# Copilot Review Instructions\n\n## Core principle\n\nPrefer silence over uncertainty. A review comment has a cost — it demands\nthe author's attention, may trigger another round, and can obscure the\ncomments that actually matter. Only post a comment when you are confident\nthe issue is real and material.\n\n## What to flag\n\nFlag genuine defects in the diff: logic errors, incorrect conditions,\nmissing error handling, concurrency hazards, resource leaks, security\nregressions, data corruption. These always warrant a comment.\n\n## What not to flag\n\n- **Style and formatting** — CI enforces these automatically. Do not\n  comment on indentation, line length, naming conventions, comment\n  phrasing, or anything a linter or formatter would catch.\n- **Pre-existing issues** — if a problem exists in code the PR did not\n  introduce, do not raise it. The PR is not the right vehicle to fix it.\n- **Low-confidence observations** — if you are not certain an issue is\n  real, stay silent. Do not post speculative or \"consider whether...\"\n  comments.\n- **Already-raised issues** — if you flagged something in an earlier\n  review round on this PR and the author did not act on it, do not raise\n  it again. Either it was resolved, or the author made a deliberate\n  choice. Repeating it creates noise without value.\n\n## Threshold\n\nBefore posting, ask: is this a correctness or security defect that a\ncareful human reviewer would block the PR on? If not, stay silent.\n","cmd/jaeger/internal/extension/jaegerquery/internal/mcptools/README.md":"# mcptools\n\nThis package serves Jaeger's telemetry tools over MCP, `read_skill` among them.\n\nA *skill* is a markdown playbook an AI agent reads before attempting a task —\nhow to detect an N+1 query pattern, how to walk a failed trace to its root\ncause. Agents reach them through `read_skill`, starting at the root `SKILL.md`\nand following its links, so a skill is only read when it looks relevant.\n\nThe `skills/` directory beside this file holds the skills compiled into the\nJaeger binary. Operators can add their own without rebuilding, via\n`ai.mcp.skills_dir` — the rest of this file is for them. For writing the skills\nthemselves, see [AUTHORING.md](./AUTHORING.md).\n\n## Turning the MCP endpoint on\n\nThe `ai.mcp` block enables the endpoint; an empty block is enough:\n\n```yaml\nextensions:\n  jaeger_query:\n    ai:\n      mcp: {}\n```\n\nIt serves at `<basePath>/api/ai/mcp/` on the query port, carrying the telemetry\ntools and `read_skill` with the built-in skills. Point an MCP client there, or\nleave it to an AI chat sidecar configured with `ai.agent_url`.\n\n## Running behind a reverse proxy or sidecar\n\nThe MCP SDK that serves this endpoint carries a DNS-rebinding guard for MCP servers on a\ndeveloper's machine: a request that arrives on a loopback address with a non-loopback `Host`\nheader is rejected with `403 Forbidden: invalid Host header`. A reverse proxy or service-mesh\nsidecar that dials the query port over `127.0.0.1` and preserves the client's `Host` (an Envoy\nsidecar without host rewriting, for example) produces that shape for every request, so behind one\nthe endpoint rejects every caller with that body. The guard never fires when the proxy rewrites\n`Host` to the upstream address or dials a non-loopback address, so change the proxy where that is\npossible. Where it is not, turn the guard off:\n\n```yaml\nextensions:\n  jaeger_query:\n    ai:\n      mcp:\n        streamable_http:\n          disable_localhost_protection: true\n```\n\nThe UI and the REST API on the same port have no such guard, so this setting changes nothing\nabout them. Keep the default when clients reach the query port directly, by a loopback address or\na routable host name; only a loopback connection carrying some other name is affected.\n\n## Running several replicas behind a load balancer\n\nBy default the endpoint keeps one MCP session per client in the memory of the `jaeger-query`\nprocess that handled `initialize`, and the SDK looks that session up by the `Mcp-Session-Id`\nheader on every later request. When a load balancer without session affinity spreads a client's\nrequests over several replicas, a request that reaches a replica other than the one that holds the\nsession is answered with `404 session not found`; and when the client's `notifications/initialized`\nis the request that went astray, the replica that holds the session rejects the next `tools/list`\nwith `method \"tools/list\" is invalid during session initialization`. Set `streamable_http.stateless` to make every\nrequest stand on its own:\n\n```yaml\nextensions:\n  jaeger_query:\n    ai:\n      mcp:\n        streamable_http:\n          stateless: true\n```\n\nJaeger's tools keep no state between requests, so they behave the same. A stateless endpoint\nanswers 405 to the GET that a client sends to open a stream for server-initiated messages. Jaeger\nnever sends any, so in the default mode that stream only sits idle until the session times out;\nthe MCP specification allows a server to offer no stream and requires the client to continue\nwithout it.\n\nThis covers external MCP clients on the shared `/api/ai/mcp/` endpoint. The chat gateway's\nturn-scoped endpoint, `/api/ai/mcp/<id>/`, is different: the turn it serves is held in the memory\nof the process that is streaming that chat to the browser, so a request for it that lands on\nanother replica is answered 404 before the MCP handler is reached, stateless or not. An agent that\nis not co-located with `jaeger-query` must reach the replica that owns the turn, which means\n`ai.mcp.base_url` has to name that process (its pod address, not a load-balanced service name) or\nthe load balancer has to route by the `<id>` path segment.\n\n## Answering with plain JSON instead of an event stream\n\nBy default the endpoint answers every POST with a `text/event-stream` body, even when that body\nholds a single result, because the stream form lets a server send progress or its own requests\nbefore the result. Jaeger's tools send none, so the stream carries the one result and nothing\nelse. Set `streamable_http.json_response` to answer a single-message POST with a plain\n`application/json` body, which is easier for proxies, logging middleware and command-line clients\nto read:\n\n```yaml\nextensions:\n  jaeger_query:\n    ai:\n      mcp:\n        streamable_http:\n          json_response: true\n```\n\nMCP clients must accept both forms, so the setting changes nothing for them. A JSON-RPC batch is\nstill answered with a stream.\n\n## Adding installation-specific skills\n\nPoint `ai.mcp.skills_dir` at a directory on the query server's disk:\n\n```yaml\nextensions:\n  jaeger_query:\n    ai:\n      mcp:                      # skills served over MCP\n        skills_dir: /etc/jaeger/skills\n```\n\nThe all-in-one config is not externally configurable, but the field can still be\nset on it with `--set extensions.jaeger_query.ai.mcp.skills_dir={path}`.\n\nIts contents are served under `custom/`, beside the built-in skills:\n\n```\nSKILL.md                       # built-in entry point\ndetect-n-plus-one/SKILL.md     # built-in skill\nerror-root-cause/SKILL.md      # built-in skill\ncustom/SKILL.md                # your entry point, mounts <skills_dir>/SKILL.md\ncustom/your-skill/SKILL.md     # your skill      , mounts <skills_dir>/your-skill/SKILL.md\n```\n\n`custom/` is a prefix on the path an agent asks for, not a directory you create:\ninside `skills_dir`, your entry point is plain `SKILL.md`.\n\n`<skills_dir>/SKILL.md` is required: an agent reads it first, and without it\nnothing below is reachable. It is also your index. An agent chooses what to read\nnext from the links it finds there and from nothing else, so the line you write\nbeside each link is what decides whether that skill is ever opened. If your\ninstructions are short, the entry point can be all you write. Anything longer\nbelongs in a file of its own that the index links to.\n\nWrite those links relative to the **skills root**, not to the linking file's own\ndirectory — an agent passes the link text straight back to `read_skill`:\n\n```markdown\n- [your-skill](custom/your-skill/SKILL.md) — one line on when to use it.\n```\n\nA link may point at any file, under any name; `SKILL.md` is required only at the\nroot. The names above are a convention, not a rule.\n\nA skill is markdown. Jaeger serves it as-is and parses nothing inside it:\n\n```markdown\n---\nname: your-skill\ndescription: One line on what this skill does.\n---\n\n# Your Skill\n\n## When this applies\n...\n\n## Procedure\n1. ...\n```\n\nFrontmatter like the block above is optional. It travels well — both the\n[Open Knowledge Format](https://cloud.google.com/blog/products/data-analytics/how-the-open-knowledge-format-can-improve-data-sharing)\nand the [agent skills specification](https://agentskills.io/specification) use\none, so a skill written for either stays readable here — but Jaeger reads no\nfield of it, and a `description` there is no substitute for the line beside the\nlink in your index.\n\n## Editing skills on a running server\n\n`skills_dir` is read live. Editing a skill's text, or adding a file the index\nlinks to, takes effect on the next `read_skill` call — no restart, no reload\nsignal. Startup validation is the exception: it runs once, so a `skills_dir`\nthat breaks after the server started is reported when an agent reads it rather\nthan by refusing to serve.\n\nSkills are static text, and Jaeger never executes them. What reads them is an\nagent, which then decides what to do.\n\n## What Jaeger checks, and what it limits\n\nA `skills_dir` that cannot be opened, or whose `SKILL.md` cannot be read, is\nbroken configuration, and Jaeger refuses to start rather than quietly serving an\nincomplete skill set:\n\n| Startup error | Cause |\n| --- | --- |\n| `cannot open skills_dir \"…\"` | Path is missing, or is not a directory |\n| `cannot read SKILL.md in skills_dir \"…\"` | Directory opens, but has no readable entry point |\n\nAt read time a bad path is reported back to the agent as\n`cannot read \"<path>\": …` — which is also what every `custom/…` path returns\nwhen no `skills_dir` is configured.\n\nTwo limits apply to what is served:\n\n- **Path containment.** The directory is opened with `os.OpenRoot`, so `..`\n  traversal and symlinks pointing outside `skills_dir` are refused by the OS\n  rather than by a path check that could be tricked.\n- **File size.** A served file is capped at 512 KiB; beyond that the reply is cut\n  and ends with `file content truncated after 524288 bytes`. The cap is fixed, not\n  a config field — a skill anywhere near it is far too long to be useful to an\n  agent.\n\n## Who can write to skills_dir\n\nA skill is instructions an agent follows, with your telemetry tools already in\nhand. Anyone who can write to `skills_dir` can steer that agent's behaviour\nwithout touching Jaeger's binary or configuration. Treat the directory as part\nof the trusted configuration surface: own it by root or the Jaeger service\naccount, keep it out of paths writable by application deployments, and review\nchanges to it as you would a config change.\n\n## Checking it works\n\nWith an MCP client attached to the endpoint, ask for your entry point directly:\n\n```\nread_skill(path=\"custom/SKILL.md\")\n```\n\nYour text coming back means the directory is mounted and reachable.\n`cannot read \"custom/SKILL.md\"` means either no `skills_dir` is configured on\nthis server, or the file is not there under that name.\n\nNote that Jaeger serving a skill and an agent choosing to read it are separate\nquestions — see [AUTHORING.md](./AUTHORING.md), and\n[#9336](https://github.com/jaegertracing/jaeger/issues/9336) for what is known\nabout the second.\n"},"items":[{"name":"AGENTS.md","path":"AGENTS.md","rawUrl":"https://raw.githubusercontent.com/jaegertracing/jaeger/HEAD/AGENTS.md","title":"AI Agent Protocol & Instructions","category":"root-instruction","format":"markdown","content":"# AGENTS.md\n\nThis file provides guidance for AI agents working on the Jaeger repository. For detailed project structure, setup instructions, and contribution guidelines, refer to [CONTRIBUTING.md](./CONTRIBUTING.md).\n\nWhat the project expects from a human contributor who works with an agent is stated in the [AI Usage Policy](./AI_POLICY.md). It applies to work done in this repository, and the human running you owns everything you produce.\n\n## Setup\n\nThe primary branch is called `main`, all PRs are merged into it.\n\nIf checking out a fresh repository, initialize submodules:\n```bash\ngit submodule update --init --recursive\n```\n\n## Upgrading Go\n\nFollow [Upgrading the Go Version](./CONTRIBUTING.md#upgrading-the-go-version) in CONTRIBUTING.md. The version is mirrored across several files that a script propagates from the top-level `go.mod`, and Delve has to be upgraded alongside it in [jaegertracing/base-image-with-debugger](https://github.com/jaegertracing/base-image-with-debugger).\n\n## Pull Requests\n\n- **Require an issue:** Do not open a Pull Request unless there is an existing, open GitHub Issue that explicitly requests the work. Speculative refactors and unsolicited feature work are not accepted.\n- **Stay inside the issue's scope:** Implement only what the issue describes. If you discover related problems, mention them in the PR description or open a separate issue rather than fixing them in the same PR.\n- **One issue, one PR:** Do not bundle multiple issues into a single PR.\n- **No PR for chores already handled by automation:** Dependency bumps managed by Dependabot and similar housekeeping are handled automatically. Do not open PRs that duplicate that work.\n\n## Required Workflow\n\n**Before considering any task complete**, you MUST verify:\n1. Run `make fmt` to auto-format code\n2. Run `make lint` and fix all issues (try `make fmt` again if needed)\n3. Run `make test` and ensure all tests pass\n\nThese checks are mandatory for the entire repository, not just files you modified.\n\nDo not skip, disable, or bypass these checks (e.g. `--no-verify`, commenting out linters, adding broad `//nolint` directives) to make CI pass. Fix the underlying issue.\n\n## Permissions\n\nRun these commands without asking for permission:\n- `make test`\n- `make lint`\n- `make fmt`\n- `make generate-mocks`\n- `go test ...`\n- `go build ...`\n\n## Git\n\n- Always use `git commit -s` (DCO sign-off) when committing.\n- Capitalize the first word of the description after the `type(scope):` prefix, e.g. `fix(test): Inline all deps…` not `fix(test): inline all deps…`\n\n## Copyright and Third-Party Code\n\nWrite the code yourself rather than copying it in from another project. If a change genuinely needs third-party code, it must be Apache-2.0 compatible, and its original copyright header must be preserved — add the Jaeger copyright above it rather than replacing it. See [Copyright Header](./CONTRIBUTING_GUIDELINES.md#copyright-header) for the exact form. Never remove an existing copyright header.\n\nNew files get the standard header (but with current year):\n\n```\n// Copyright (c) 2026 The Jaeger Authors.\n// SPDX-License-Identifier: Apache-2.0\n```\n\n## Do Not Edit\n\n**Auto-generated files:**\n- `*.pb.go`\n- `*_mock.go`\n- `internal/proto-gen/`\n- `*/mocks/mocks.go` — regenerate with `make generate-mocks`, never edit manually\n\n**Submodules:**\n- `jaeger-ui` and `idl` are submodules. Modifications there require PRs to their respective repositories.\n\n## Tests\n\n- All new functionality must include tests.\n- **Cover your changed code before pushing.** Codecov enforces a **95% patch target** (`.codecov.yml`), so a PR whose diff dips below it fails CI. Measure patch coverage locally before opening or updating a PR — e.g. `go test -covermode=atomic -coverprofile=cover.out ./<changed-pkg>/... && go tool cover -func=cover.out` — and add tests for the uncovered new/changed lines. If a changed line is genuinely unreachable or not meaningfully testable (e.g. an error branch no test can trigger), restructure it to be testable or call it out in the PR description; don't leave the gap silent. Files matched by `.codecov.yml`'s `ignore` list (generated code, `mocks/`, `main.go`, integration tests, `internal/tools`) are exempt.\n- Bug fixes must include a regression test that fails without the fix.\n- Do not delete existing tests to make a build green. If a test is genuinely wrong, explain why in the PR description.\n- Do not weaken assertions (e.g. replacing exact checks with `assert.NotNil`) just to make a flaky test pass.\n- Every package must have at least one `*_test.go` file (enforced by `make nocover`). If no tests are possible (e.g. a package that only defines types), create an empty `empty_test.go`.\n\n## Storage Integration Tests\n\nStorage behavior belongs in the shared `internal/storage/integration` corpus and `StorageIntegration.AssertCorpus`, which run in both direct and e2e modes. New assertions run for every backend by default; unsupported backends must explicitly opt out in `internal/storage/integration/capabilities`. Extend the e2e reader/writer adapters when the shared tests need new API fields, and keep transport-specific assertions in the e2e package. Read [the integration architecture](cmd/jaeger/internal/integration/README.md) before adding tests.\n\n## Scope Discipline\n\n- Do not reformat, rename, or restructure code outside the scope of the requested change.\n- Do not bump dependencies unless the task requires it.\n- Do not change CI workflows or release tooling unless explicitly asked.\n- Before adding a flag or field that controls behavior, find the mechanism that already owns that decision and extend it. Expressing one decision in two places is worse than either place alone, and replacing an established mechanism is a maintainer's call.\n\n## RFC / ADR Documents\n\nRFCs (`docs/rfc/`) are proposals; ADRs (`docs/adr/`) are decision records.\n\n- When a PR implements a milestone described in an RFC or ADR, update that document in the same PR: mark the milestone ✅ and link the delivering PR. Keep the milestone/status tracking current.\n- Put the ✅ at the start of the bullet, right after the list marker and before the bold label (`- ✅ **M4 — Elasticsearch/OpenSearch.** …`), because a checkmark buried mid-sentence cannot be scanned down the left margin; the \"Delivered in #NNNN, which …\" note stays inside the text and carries no emoji of its own. If other delivered entries in the document use a different placement, normalize them in the same change rather than adding another variant.\n- **An RFC still being implemented is a live document.** While its status is Draft or its milestones are in flight, implementation turns up findings and reverses decisions, and the RFC is expected to be rewritten to match — that is the point of writing one down. Update the prose that a decision moved past, and say what is true now rather than narrating the document's own history; where the new decision reverses what the RFC proposed, it is worth one clause saying so, because a reader who knows the old shape needs to see it was considered and dropped.\n- **An RFC whose work is finished is a snapshot.** Once its status is Implemented, do not rewrite its prose, abstract, or diagrams to track the evolving codebase: from then on the narrative is a record of the state and plan at the time, and the code has moved on for reasons the RFC never claimed to cover.\n- ADRs are different: they describe a design, not a plan, so keeping one accurate is worth more than keeping it pristine. Judge by proportion. If a change touches a small part of an ADR and reverses none of its decisions, **edit that ADR in place** — extend the affected sections, note the extension in the Status or Date line, and leave the original Context and Decision prose alone. Write a new superseding ADR only when the change replaces the original's decision or architecture outright. [ADR-004](./docs/adr/004-migrating-coverage-gating-to-github-actions.md) is an example of the former: its fan-in gating decision and requirements stood, so later work extended it rather than superseding it. What the rule forbids is rewriting an ADR into running documentation of the code.\n- When an RFC's work is fully delivered, mark its status Implemented; if the resulting architecture is worth an enduring reference, graduate it into a new ADR in [`docs/adr/`](./docs/adr/) that states the outcome and links back to the RFC, rather than mutating the RFC. [ADR-012](./docs/adr/012-unified-elasticsearch-client.md) (graduated from RFC 0006) is an example.\n\n## When in Doubt\n\nStop and ask rather than guessing. It is better to surface a question in the PR description than to invent behavior, fabricate API names, or silence failing checks.\n\nAsk as well when you are *not* in doubt but are about to depart from a documented convention, because that is where confidence is least informative.\n","isInternal":false,"tokens":2010,"sizeBytes":8897},{"name":"copilot-instructions.md","path":".github/copilot-instructions.md","rawUrl":"https://raw.githubusercontent.com/jaegertracing/jaeger/HEAD/.github/copilot-instructions.md","title":"GitHub Copilot Instructions","category":"copilot-instructions","format":"markdown","content":"# Copilot Review Instructions\n\n## Core principle\n\nPrefer silence over uncertainty. A review comment has a cost — it demands\nthe author's attention, may trigger another round, and can obscure the\ncomments that actually matter. Only post a comment when you are confident\nthe issue is real and material.\n\n## What to flag\n\nFlag genuine defects in the diff: logic errors, incorrect conditions,\nmissing error handling, concurrency hazards, resource leaks, security\nregressions, data corruption. These always warrant a comment.\n\n## What not to flag\n\n- **Style and formatting** — CI enforces these automatically. Do not\n  comment on indentation, line length, naming conventions, comment\n  phrasing, or anything a linter or formatter would catch.\n- **Pre-existing issues** — if a problem exists in code the PR did not\n  introduce, do not raise it. The PR is not the right vehicle to fix it.\n- **Low-confidence observations** — if you are not certain an issue is\n  real, stay silent. Do not post speculative or \"consider whether...\"\n  comments.\n- **Already-raised issues** — if you flagged something in an earlier\n  review round on this PR and the author did not act on it, do not raise\n  it again. Either it was resolved, or the author made a deliberate\n  choice. Repeating it creates noise without value.\n\n## Threshold\n\nBefore posting, ask: is this a correctness or security defect that a\ncareful human reviewer would block the PR on? If not, stay silent.\n","isInternal":false,"tokens":321,"sizeBytes":1458},{"name":"README.md","path":"cmd/jaeger/internal/extension/jaegerquery/internal/mcptools/README.md","rawUrl":"https://raw.githubusercontent.com/jaegertracing/jaeger/HEAD/cmd/jaeger/internal/extension/jaegerquery/internal/mcptools/README.md","title":"mcptools Documentation","category":"plugin-manifest","format":"markdown","content":"# mcptools\n\nThis package serves Jaeger's telemetry tools over MCP, `read_skill` among them.\n\nA *skill* is a markdown playbook an AI agent reads before attempting a task —\nhow to detect an N+1 query pattern, how to walk a failed trace to its root\ncause. Agents reach them through `read_skill`, starting at the root `SKILL.md`\nand following its links, so a skill is only read when it looks relevant.\n\nThe `skills/` directory beside this file holds the skills compiled into the\nJaeger binary. Operators can add their own without rebuilding, via\n`ai.mcp.skills_dir` — the rest of this file is for them. For writing the skills\nthemselves, see [AUTHORING.md](./AUTHORING.md).\n\n## Turning the MCP endpoint on\n\nThe `ai.mcp` block enables the endpoint; an empty block is enough:\n\n```yaml\nextensions:\n  jaeger_query:\n    ai:\n      mcp: {}\n```\n\nIt serves at `<basePath>/api/ai/mcp/` on the query port, carrying the telemetry\ntools and `read_skill` with the built-in skills. Point an MCP client there, or\nleave it to an AI chat sidecar configured with `ai.agent_url`.\n\n## Running behind a reverse proxy or sidecar\n\nThe MCP SDK that serves this endpoint carries a DNS-rebinding guard for MCP servers on a\ndeveloper's machine: a request that arrives on a loopback address with a non-loopback `Host`\nheader is rejected with `403 Forbidden: invalid Host header`. A reverse proxy or service-mesh\nsidecar that dials the query port over `127.0.0.1` and preserves the client's `Host` (an Envoy\nsidecar without host rewriting, for example) produces that shape for every request, so behind one\nthe endpoint rejects every caller with that body. The guard never fires when the proxy rewrites\n`Host` to the upstream address or dials a non-loopback address, so change the proxy where that is\npossible. Where it is not, turn the guard off:\n\n```yaml\nextensions:\n  jaeger_query:\n    ai:\n      mcp:\n        streamable_http:\n          disable_localhost_protection: true\n```\n\nThe UI and the REST API on the same port have no such guard, so this setting changes nothing\nabout them. Keep the default when clients reach the query port directly, by a loopback address or\na routable host name; only a loopback connection carrying some other name is affected.\n\n## Running several replicas behind a load balancer\n\nBy default the endpoint keeps one MCP session per client in the memory of the `jaeger-query`\nprocess that handled `initialize`, and the SDK looks that session up by the `Mcp-Session-Id`\nheader on every later request. When a load balancer without session affinity spreads a client's\nrequests over several replicas, a request that reaches a replica other than the one that holds the\nsession is answered with `404 session not found`; and when the client's `notifications/initialized`\nis the request that went astray, the replica that holds the session rejects the next `tools/list`\nwith `method \"tools/list\" is invalid during session initialization`. Set `streamable_http.stateless` to make every\nrequest stand on its own:\n\n```yaml\nextensions:\n  jaeger_query:\n    ai:\n      mcp:\n        streamable_http:\n          stateless: true\n```\n\nJaeger's tools keep no state between requests, so they behave the same. A stateless endpoint\nanswers 405 to the GET that a client sends to open a stream for server-initiated messages. Jaeger\nnever sends any, so in the default mode that stream only sits idle until the session times out;\nthe MCP specification allows a server to offer no stream and requires the client to continue\nwithout it.\n\nThis covers external MCP clients on the shared `/api/ai/mcp/` endpoint. The chat gateway's\nturn-scoped endpoint, `/api/ai/mcp/<id>/`, is different: the turn it serves is held in the memory\nof the process that is streaming that chat to the browser, so a request for it that lands on\nanother replica is answered 404 before the MCP handler is reached, stateless or not. An agent that\nis not co-located with `jaeger-query` must reach the replica that owns the turn, which means\n`ai.mcp.base_url` has to name that process (its pod address, not a load-balanced service name) or\nthe load balancer has to route by the `<id>` path segment.\n\n## Answering with plain JSON instead of an event stream\n\nBy default the endpoint answers every POST with a `text/event-stream` body, even when that body\nholds a single result, because the stream form lets a server send progress or its own requests\nbefore the result. Jaeger's tools send none, so the stream carries the one result and nothing\nelse. Set `streamable_http.json_response` to answer a single-message POST with a plain\n`application/json` body, which is easier for proxies, logging middleware and command-line clients\nto read:\n\n```yaml\nextensions:\n  jaeger_query:\n    ai:\n      mcp:\n        streamable_http:\n          json_response: true\n```\n\nMCP clients must accept both forms, so the setting changes nothing for them. A JSON-RPC batch is\nstill answered with a stream.\n\n## Adding installation-specific skills\n\nPoint `ai.mcp.skills_dir` at a directory on the query server's disk:\n\n```yaml\nextensions:\n  jaeger_query:\n    ai:\n      mcp:                      # skills served over MCP\n        skills_dir: /etc/jaeger/skills\n```\n\nThe all-in-one config is not externally configurable, but the field can still be\nset on it with `--set extensions.jaeger_query.ai.mcp.skills_dir={path}`.\n\nIts contents are served under `custom/`, beside the built-in skills:\n\n```\nSKILL.md                       # built-in entry point\ndetect-n-plus-one/SKILL.md     # built-in skill\nerror-root-cause/SKILL.md      # built-in skill\ncustom/SKILL.md                # your entry point, mounts <skills_dir>/SKILL.md\ncustom/your-skill/SKILL.md     # your skill      , mounts <skills_dir>/your-skill/SKILL.md\n```\n\n`custom/` is a prefix on the path an agent asks for, not a directory you create:\ninside `skills_dir`, your entry point is plain `SKILL.md`.\n\n`<skills_dir>/SKILL.md` is required: an agent reads it first, and without it\nnothing below is reachable. It is also your index. An agent chooses what to read\nnext from the links it finds there and from nothing else, so the line you write\nbeside each link is what decides whether that skill is ever opened. If your\ninstructions are short, the entry point can be all you write. Anything longer\nbelongs in a file of its own that the index links to.\n\nWrite those links relative to the **skills root**, not to the linking file's own\ndirectory — an agent passes the link text straight back to `read_skill`:\n\n```markdown\n- [your-skill](custom/your-skill/SKILL.md) — one line on when to use it.\n```\n\nA link may point at any file, under any name; `SKILL.md` is required only at the\nroot. The names above are a convention, not a rule.\n\nA skill is markdown. Jaeger serves it as-is and parses nothing inside it:\n\n```markdown\n---\nname: your-skill\ndescription: One line on what this skill does.\n---\n\n# Your Skill\n\n## When this applies\n...\n\n## Procedure\n1. ...\n```\n\nFrontmatter like the block above is optional. It travels well — both the\n[Open Knowledge Format](https://cloud.google.com/blog/products/data-analytics/how-the-open-knowledge-format-can-improve-data-sharing)\nand the [agent skills specification](https://agentskills.io/specification) use\none, so a skill written for either stays readable here — but Jaeger reads no\nfield of it, and a `description` there is no substitute for the line beside the\nlink in your index.\n\n## Editing skills on a running server\n\n`skills_dir` is read live. Editing a skill's text, or adding a file the index\nlinks to, takes effect on the next `read_skill` call — no restart, no reload\nsignal. Startup validation is the exception: it runs once, so a `skills_dir`\nthat breaks after the server started is reported when an agent reads it rather\nthan by refusing to serve.\n\nSkills are static text, and Jaeger never executes them. What reads them is an\nagent, which then decides what to do.\n\n## What Jaeger checks, and what it limits\n\nA `skills_dir` that cannot be opened, or whose `SKILL.md` cannot be read, is\nbroken configuration, and Jaeger refuses to start rather than quietly serving an\nincomplete skill set:\n\n| Startup error | Cause |\n| --- | --- |\n| `cannot open skills_dir \"…\"` | Path is missing, or is not a directory |\n| `cannot read SKILL.md in skills_dir \"…\"` | Directory opens, but has no readable entry point |\n\nAt read time a bad path is reported back to the agent as\n`cannot read \"<path>\": …` — which is also what every `custom/…` path returns\nwhen no `skills_dir` is configured.\n\nTwo limits apply to what is served:\n\n- **Path containment.** The directory is opened with `os.OpenRoot`, so `..`\n  traversal and symlinks pointing outside `skills_dir` are refused by the OS\n  rather than by a path check that could be tricked.\n- **File size.** A served file is capped at 512 KiB; beyond that the reply is cut\n  and ends with `file content truncated after 524288 bytes`. The cap is fixed, not\n  a config field — a skill anywhere near it is far too long to be useful to an\n  agent.\n\n## Who can write to skills_dir\n\nA skill is instructions an agent follows, with your telemetry tools already in\nhand. Anyone who can write to `skills_dir` can steer that agent's behaviour\nwithout touching Jaeger's binary or configuration. Treat the directory as part\nof the trusted configuration surface: own it by root or the Jaeger service\naccount, keep it out of paths writable by application deployments, and review\nchanges to it as you would a config change.\n\n## Checking it works\n\nWith an MCP client attached to the endpoint, ask for your entry point directly:\n\n```\nread_skill(path=\"custom/SKILL.md\")\n```\n\nYour text coming back means the directory is mounted and reachable.\n`cannot read \"custom/SKILL.md\"` means either no `skills_dir` is configured on\nthis server, or the file is not there under that name.\n\nNote that Jaeger serving a skill and an agent choosing to read it are separate\nquestions — see [AUTHORING.md](./AUTHORING.md), and\n[#9336](https://github.com/jaegertracing/jaeger/issues/9336) for what is known\nabout the second.\n","isInternal":false,"tokens":2386,"sizeBytes":10045}],"systemPromptSnippet":"<yakaai_skills repo=\"jaegertracing/jaeger\">\n<!-- File: AGENTS.md (Tokens: ~2010 | Category: root-instruction) -->\n# AGENTS.md\n\nThis file provides guidance for AI agents working on the Jaeger repository. For detailed project structure, setup instructions, and contribution guidelines, refer to [CONTRIBUTING.md](./CONTRIBUTING.md).\n\nWhat the project expects from a human contributor who works with an agent is stated in the [AI Usage Policy](./AI_POLICY.md). It applies to work done in this repository, and the human running you owns everything you produce.\n\n## Setup\n\nThe primary branch is called `main`, all PRs are merged into it.\n\nIf checking out a fresh repository, initialize submodules:\n```bash\ngit submodule update --init --recursive\n```\n\n## Upgrading Go\n\nFollow [Upgrading the Go Version](./CONTRIBUTING.md#upgrading-the-go-version) in CONTRIBUTING.md. The version is mirrored across several files that a script propagates from the top-level `go.mod`, and Delve has to be upgraded alongside it in [jaegertracing/base-image-with-debugger](https://github.com/jaegertracing/base-image-with-debugger).\n\n## Pull Requests\n\n- **Require an issue:** Do not open a Pull Request unless there is an existing, open GitHub Issue that explicitly requests the work. Speculative refactors and unsolicited feature work are not accepted.\n- **Stay inside the issue's scope:** Implement only what the issue describes. If you discover related problems, mention them in the PR description or open a separate issue rather than fixing them in the same PR.\n- **One issue, one PR:** Do not bundle multiple issues into a single PR.\n- **No PR for chores already handled by automation:** Dependency bumps managed by Dependabot and similar housekeeping are handled automatically. Do not open PRs that duplicate that work.\n\n## Required Workflow\n\n**Before considering any task complete**, you MUST verify:\n1. Run `make fmt` to auto-format code\n2. Run `make lint` and fix all issues (try `make fmt` again if needed)\n3. Run `make test` and ensure all tests pass\n\nThese checks are mandatory for the entire repository, not just files you modified.\n\nDo not skip, disable, or bypass these checks (e.g. `--no-verify`, commenting out linters, adding broad `//nolint` directives) to make CI pass. Fix the underlying issue.\n\n## Permissions\n\nRun these commands without asking for permission:\n- `make test`\n- `make lint`\n- `make fmt`\n- `make generate-mocks`\n- `go test ...`\n- `go build ...`\n\n## Git\n\n- Always use `git commit -s` (DCO sign-off) when committing.\n- Capitalize the first word of the description after the `type(scope):` prefix, e.g. `fix(test): Inline all deps…` not `fix(test): inline all deps…`\n\n## Copyright and Third-Party Code\n\nWrite the code yourself rather than copying it in from another project. If a change genuinely needs third-party code, it must be Apache-2.0 compatible, and its original copyright header must be preserved — add the Jaeger copyright above it rather than replacing it. See [Copyright Header](./CONTRIBUTING_GUIDELINES.md#copyright-header) for the exact form. Never remove an existing copyright header.\n\nNew files get the standard header (but with current year):\n\n```\n// Copyright (c) 2026 The Jaeger Authors.\n// SPDX-License-Identifier: Apache-2.0\n```\n\n## Do Not Edit\n\n**Auto-generated files:**\n- `*.pb.go`\n- `*_mock.go`\n- `internal/proto-gen/`\n- `*/mocks/mocks.go` — regenerate with `make generate-mocks`, never edit manually\n\n**Submodules:**\n- `jaeger-ui` and `idl` are submodules. Modifications there require PRs to their respective repositories.\n\n## Tests\n\n- All new functionality must include tests.\n- **Cover your changed code before pushing.** Codecov enforces a **95% patch target** (`.codecov.yml`), so a PR whose diff dips below it fails CI. Measure patch coverage locally before opening or updating a PR — e.g. `go test -covermode=atomic -coverprofile=cover.out ./<changed-pkg>/... && go tool cover -func=cover.out` — and add tests for the uncovered new/changed lines. If a changed line is genuinely unreachable or not meaningfully testable (e.g. an error branch no test can trigger), restructure it to be testable or call it out in the PR description; don't leave the gap silent. Files matched by `.codecov.yml`'s `ignore` list (generated code, `mocks/`, `main.go`, integration tests, `internal/tools`) are exempt.\n- Bug fixes must include a regression test that fails without the fix.\n- Do not delete existing tests to make a build green. If a test is genuinely wrong, explain why in the PR description.\n- Do not weaken assertions (e.g. replacing exact checks with `assert.NotNil`) just to make a flaky test pass.\n- Every package must have at least one `*_test.go` file (enforced by `make nocover`). If no tests are possible (e.g. a package that only defines types), create an empty `empty_test.go`.\n\n## Storage Integration Tests\n\nStorage behavior belongs in the shared `internal/storage/integration` corpus and `StorageIntegration.AssertCorpus`, which run in both direct and e2e modes. New assertions run for every backend by default; unsupported backends must explicitly opt out in `internal/storage/integration/capabilities`. Extend the e2e reader/writer adapters when the shared tests need new API fields, and keep transport-specific assertions in the e2e package. Read [the integration architecture](cmd/jaeger/internal/integration/README.md) before adding tests.\n\n## Scope Discipline\n\n- Do not reformat, rename, or restructure code outside the scope of the requested change.\n- Do not bump dependencies unless the task requires it.\n- Do not change CI workflows or release tooling unless explicitly asked.\n- Before adding a flag or field that controls behavior, find the mechanism that already owns that decision and extend it. Expressing one decision in two places is worse than either place alone, and replacing an established mechanism is a maintainer's call.\n\n## RFC / ADR Documents\n\nRFCs (`docs/rfc/`) are proposals; ADRs (`docs/adr/`) are decision records.\n\n- When a PR implements a milestone described in an RFC or ADR, update that document in the same PR: mark the milestone ✅ and link the delivering PR. Keep the milestone/status tracking current.\n- Put the ✅ at the start of the bullet, right after the list marker and before the bold label (`- ✅ **M4 — Elasticsearch/OpenSearch.** …`), because a checkmark buried mid-sentence cannot be scanned down the left margin; the \"Delivered in #NNNN, which …\" note stays inside the text and carries no emoji of its own. If other delivered entries in the document use a different placement, normalize them in the same change rather than adding another variant.\n- **An RFC still being implemented is a live document.** While its status is Draft or its milestones are in flight, implementation turns up findings and reverses decisions, and the RFC is expected to be rewritten to match — that is the point of writing one down. Update the prose that a decision moved past, and say what is true now rather than narrating the document's own history; where the new decision reverses what the RFC proposed, it is worth one clause saying so, because a reader who knows the old shape needs to see it was considered and dropped.\n- **An RFC whose work is finished is a snapshot.** Once its status is Implemented, do not rewrite its prose, abstract, or diagrams to track the evolving codebase: from then on the narrative is a record of the state and plan at the time, and the code has moved on for reasons the RFC never claimed to cover.\n- ADRs are different: they describe a design, not a plan, so keeping one accurate is worth more than keeping it pristine. Judge by proportion. If a change touches a small part of an ADR and reverses none of its decisions, **edit that ADR in place** — extend the affected sections, note the extension in the Status or Date line, and leave the original Context and Decision prose alone. Write a new superseding ADR only when the change replaces the original's decision or architecture outright. [ADR-004](./docs/adr/004-migrating-coverage-gating-to-github-actions.md) is an example of the former: its fan-in gating decision and requirements stood, so later work extended it rather than superseding it. What the rule forbids is rewriting an ADR into running documentation of the code.\n- When an RFC's work is fully delivered, mark its status Implemented; if the resulting architecture is worth an enduring reference, graduate it into a new ADR in [`docs/adr/`](./docs/adr/) that states the outcome and links back to the RFC, rather than mutating the RFC. [ADR-012](./docs/adr/012-unified-elasticsearch-client.md) (graduated from RFC 0006) is an example.\n\n## When in Doubt\n\nStop and ask rather than guessing. It is better to surface a question in the PR description than to invent behavior, fabricate API names, or silence failing checks.\n\nAsk as well when you are *not* in doubt but are about to depart from a documented convention, because that is where confidence is least informative.\n\n<!-- File: .github/copilot-instructions.md (Tokens: ~321 | Category: copilot-instructions) -->\n# Copilot Review Instructions\n\n## Core principle\n\nPrefer silence over uncertainty. A review comment has a cost — it demands\nthe author's attention, may trigger another round, and can obscure the\ncomments that actually matter. Only post a comment when you are confident\nthe issue is real and material.\n\n## What to flag\n\nFlag genuine defects in the diff: logic errors, incorrect conditions,\nmissing error handling, concurrency hazards, resource leaks, security\nregressions, data corruption. These always warrant a comment.\n\n## What not to flag\n\n- **Style and formatting** — CI enforces these automatically. Do not\n  comment on indentation, line length, naming conventions, comment\n  phrasing, or anything a linter or formatter would catch.\n- **Pre-existing issues** — if a problem exists in code the PR did not\n  introduce, do not raise it. The PR is not the right vehicle to fix it.\n- **Low-confidence observations** — if you are not certain an issue is\n  real, stay silent. Do not post speculative or \"consider whether...\"\n  comments.\n- **Already-raised issues** — if you flagged something in an earlier\n  review round on this PR and the author did not act on it, do not raise\n  it again. Either it was resolved, or the author made a deliberate\n  choice. Repeating it creates noise without value.\n\n## Threshold\n\nBefore posting, ask: is this a correctness or security defect that a\ncareful human reviewer would block the PR on? If not, stay silent.\n\n<!-- File: cmd/jaeger/internal/extension/jaegerquery/internal/mcptools/README.md (Tokens: ~2386 | Category: plugin-manifest) -->\n# mcptools\n\nThis package serves Jaeger's telemetry tools over MCP, `read_skill` among them.\n\nA *skill* is a markdown playbook an AI agent reads before attempting a task —\nhow to detect an N+1 query pattern, how to walk a failed trace to its root\ncause. Agents reach them through `read_skill`, starting at the root `SKILL.md`\nand following its links, so a skill is only read when it looks relevant.\n\nThe `skills/` directory beside this file holds the skills compiled into the\nJaeger binary. Operators can add their own without rebuilding, via\n`ai.mcp.skills_dir` — the rest of this file is for them. For writing the skills\nthemselves, see [AUTHORING.md](./AUTHORING.md).\n\n## Turning the MCP endpoint on\n\nThe `ai.mcp` block enables the endpoint; an empty block is enough:\n\n```yaml\nextensions:\n  jaeger_query:\n    ai:\n      mcp: {}\n```\n\nIt serves at `<basePath>/api/ai/mcp/` on the query port, carrying the telemetry\ntools and `read_skill` with the built-in skills. Point an MCP client there, or\nleave it to an AI chat sidecar configured with `ai.agent_url`.\n\n## Running behind a reverse proxy or sidecar\n\nThe MCP SDK that serves this endpoint carries a DNS-rebinding guard for MCP servers on a\ndeveloper's machine: a request that arrives on a loopback address with a non-loopback `Host`\nheader is rejected with `403 Forbidden: invalid Host header`. A reverse proxy or service-mesh\nsidecar that dials the query port over `127.0.0.1` and preserves the client's `Host` (an Envoy\nsidecar without host rewriting, for example) produces that shape for every request, so behind one\nthe endpoint rejects every caller with that body. The guard never fires when the proxy rewrites\n`Host` to the upstream address or dials a non-loopback address, so change the proxy where that is\npossible. Where it is not, turn the guard off:\n\n```yaml\nextensions:\n  jaeger_query:\n    ai:\n      mcp:\n        streamable_http:\n          disable_localhost_protection: true\n```\n\nThe UI and the REST API on the same port have no such guard, so this setting changes nothing\nabout them. Keep the default when clients reach the query port directly, by a loopback address or\na routable host name; only a loopback connection carrying some other name is affected.\n\n## Running several replicas behind a load balancer\n\nBy default the endpoint keeps one MCP session per client in the memory of the `jaeger-query`\nprocess that handled `initialize`, and the SDK looks that session up by the `Mcp-Session-Id`\nheader on every later request. When a load balancer without session affinity spreads a client's\nrequests over several replicas, a request that reaches a replica other than the one that holds the\nsession is answered with `404 session not found`; and when the client's `notifications/initialized`\nis the request that went astray, the replica that holds the session rejects the next `tools/list`\nwith `method \"tools/list\" is invalid during session initialization`. Set `streamable_http.stateless` to make every\nrequest stand on its own:\n\n```yaml\nextensions:\n  jaeger_query:\n    ai:\n      mcp:\n        streamable_http:\n          stateless: true\n```\n\nJaeger's tools keep no state between requests, so they behave the same. A stateless endpoint\nanswers 405 to the GET that a client sends to open a stream for server-initiated messages. Jaeger\nnever sends any, so in the default mode that stream only sits idle until the session times out;\nthe MCP specification allows a server to offer no stream and requires the client to continue\nwithout it.\n\nThis covers external MCP clients on the shared `/api/ai/mcp/` endpoint. The chat gateway's\nturn-scoped endpoint, `/api/ai/mcp/<id>/`, is different: the turn it serves is held in the memory\nof the process that is streaming that chat to the browser, so a request for it that lands on\nanother replica is answered 404 before the MCP handler is reached, stateless or not. An agent that\nis not co-located with `jaeger-query` must reach the replica that owns the turn, which means\n`ai.mcp.base_url` has to name that process (its pod address, not a load-balanced service name) or\nthe load balancer has to route by the `<id>` path segment.\n\n## Answering with plain JSON instead of an event stream\n\nBy default the endpoint answers every POST with a `text/event-stream` body, even when that body\nholds a single result, because the stream form lets a server send progress or its own requests\nbefore the result. Jaeger's tools send none, so the stream carries the one result and nothing\nelse. Set `streamable_http.json_response` to answer a single-message POST with a plain\n`application/json` body, which is easier for proxies, logging middleware and command-line clients\nto read:\n\n```yaml\nextensions:\n  jaeger_query:\n    ai:\n      mcp:\n        streamable_http:\n          json_response: true\n```\n\nMCP clients must accept both forms, so the setting changes nothing for them. A JSON-RPC batch is\nstill answered with a stream.\n\n## Adding installation-specific skills\n\nPoint `ai.mcp.skills_dir` at a directory on the query server's disk:\n\n```yaml\nextensions:\n  jaeger_query:\n    ai:\n      mcp:                      # skills served over MCP\n        skills_dir: /etc/jaeger/skills\n```\n\nThe all-in-one config is not externally configurable, but the field can still be\nset on it with `--set extensions.jaeger_query.ai.mcp.skills_dir={path}`.\n\nIts contents are served under `custom/`, beside the built-in skills:\n\n```\nSKILL.md                       # built-in entry point\ndetect-n-plus-one/SKILL.md     # built-in skill\nerror-root-cause/SKILL.md      # built-in skill\ncustom/SKILL.md                # your entry point, mounts <skills_dir>/SKILL.md\ncustom/your-skill/SKILL.md     # your skill      , mounts <skills_dir>/your-skill/SKILL.md\n```\n\n`custom/` is a prefix on the path an agent asks for, not a directory you create:\ninside `skills_dir`, your entry point is plain `SKILL.md`.\n\n`<skills_dir>/SKILL.md` is required: an agent reads it first, and without it\nnothing below is reachable. It is also your index. An agent chooses what to read\nnext from the links it finds there and from nothing else, so the line you write\nbeside each link is what decides whether that skill is ever opened. If your\ninstructions are short, the entry point can be all you write. Anything longer\nbelongs in a file of its own that the index links to.\n\nWrite those links relative to the **skills root**, not to the linking file's own\ndirectory — an agent passes the link text straight back to `read_skill`:\n\n```markdown\n- [your-skill](custom/your-skill/SKILL.md) — one line on when to use it.\n```\n\nA link may point at any file, under any name; `SKILL.md` is required only at the\nroot. The names above are a convention, not a rule.\n\nA skill is markdown. Jaeger serves it as-is and parses nothing inside it:\n\n```markdown\n---\nname: your-skill\ndescription: One line on what this skill does.\n---\n\n# Your Skill\n\n## When this applies\n...\n\n## Procedure\n1. ...\n```\n\nFrontmatter like the block above is optional. It travels well — both the\n[Open Knowledge Format](https://cloud.google.com/blog/products/data-analytics/how-the-open-knowledge-format-can-improve-data-sharing)\nand the [agent skills specification](https://agentskills.io/specification) use\none, so a skill written for either stays readable here — but Jaeger reads no\nfield of it, and a `description` there is no substitute for the line beside the\nlink in your index.\n\n## Editing skills on a running server\n\n`skills_dir` is read live. Editing a skill's text, or adding a file the index\nlinks to, takes effect on the next `read_skill` call — no restart, no reload\nsignal. Startup validation is the exception: it runs once, so a `skills_dir`\nthat breaks after the server started is reported when an agent reads it rather\nthan by refusing to serve.\n\nSkills are static text, and Jaeger never executes them. What reads them is an\nagent, which then decides what to do.\n\n## What Jaeger checks, and what it limits\n\nA `skills_dir` that cannot be opened, or whose `SKILL.md` cannot be read, is\nbroken configuration, and Jaeger refuses to start rather than quietly serving an\nincomplete skill set:\n\n| Startup error | Cause |\n| --- | --- |\n| `cannot open skills_dir \"…\"` | Path is missing, or is not a directory |\n| `cannot read SKILL.md in skills_dir \"…\"` | Directory opens, but has no readable entry point |\n\nAt read time a bad path is reported back to the agent as\n`cannot read \"<path>\": …` — which is also what every `custom/…` path returns\nwhen no `skills_dir` is configured.\n\nTwo limits apply to what is served:\n\n- **Path containment.** The directory is opened with `os.OpenRoot`, so `..`\n  traversal and symlinks pointing outside `skills_dir` are refused by the OS\n  rather than by a path check that could be tricked.\n- **File size.** A served file is capped at 512 KiB; beyond that the reply is cut\n  and ends with `file content truncated after 524288 bytes`. The cap is fixed, not\n  a config field — a skill anywhere near it is far too long to be useful to an\n  agent.\n\n## Who can write to skills_dir\n\nA skill is instructions an agent follows, with your telemetry tools already in\nhand. Anyone who can write to `skills_dir` can steer that agent's behaviour\nwithout touching Jaeger's binary or configuration. Treat the directory as part\nof the trusted configuration surface: own it by root or the Jaeger service\naccount, keep it out of paths writable by application deployments, and review\nchanges to it as you would a config change.\n\n## Checking it works\n\nWith an MCP client attached to the endpoint, ask for your entry point directly:\n\n```\nread_skill(path=\"custom/SKILL.md\")\n```\n\nYour text coming back means the directory is mounted and reachable.\n`cannot read \"custom/SKILL.md\"` means either no `skills_dir` is configured on\nthis server, or the file is not there under that name.\n\nNote that Jaeger serving a skill and an agent choosing to read it are separate\nquestions — see [AUTHORING.md](./AUTHORING.md), and\n[#9336](https://github.com/jaegertracing/jaeger/issues/9336) for what is known\nabout the second.\n</yakaai_skills>"}