Microscope plugin
Read JVM profiles from a running Jeffrey Microscope
without leaving your terminal: point your coding agent at a .jfr file in your repository and it
analyses it, then lists the recordings you have analysed, queries their DuckDB tables, and pulls
flamegraph, trace and heap-dump exports straight into a session in your own repository — so the
profile and the source code are in front of the same reader.
One package, four manifests. Claude Code reads .claude-plugin/plugin.json; Codex reads
.codex-plugin/plugin.json; the other Agent Plugins clients read the
root plugin.json and mcp.json; Gemini CLI reads gemini-extension.json. The skills and the
MCP server underneath are the same files for all of them.
Jeffrey speaks MCP 2026-07-28 only. A client that still opens with initialize is refused with
-32602 (HTTP 400), an error whose message and data.supported name that version, so which clients
can connect today is narrower than which clients can install the package:
- Claude Code — on its v2 MCP runtime only; the v1 runtime cannot talk to a server that speaks
only
2026-07-28. v2 is the default on Claude Code v2.1.274 or later for Bedrock, Claude Platform on AWS, Google Cloud's Agent Platform, Microsoft Foundry, Claude apps gateway sessions and sessions with telemetry or feature-flag fetching off. Anywhere else, start it withMCP_SDK_GENERATION=v2(see Claude Code's MCP docs). - Codex — v0.147.0 or later, with the opt-in protocol turned on. It is a global feature flag,
not a per-server setting:
[features] mcp_2026_07_28 = truein~/.codex/config.toml, orcodex --enable mcp_2026_07_28. It applies to every HTTP MCP server Codex connects to, and Codex marks it as under development. - Gemini CLI — cannot connect to this Jeffrey until it supports MCP
2026-07-28. The extension files stay here so it works once Gemini CLI does. - Anything else (Cursor, VS Code, Kiro, …) — works with any client that speaks MCP
2026-07-28over Streamable HTTP.
Every analysis tool is read-only, and every tool says so in its MCP annotations rather than
leaving a client to infer it. Eleven do not read: recordings_analyzeFile and recordings_analyzeRecording, which create profiles
rather than changing them, recordings_delete, the one destructive tool, which removes a recording and the
profile built from it, heap_prepare, which writes only a cache, heap_oql with includeRetainedSize
and jvm_autoAnalysis with compute, which fill the dominator tree and the Auto Analysis cache and run as
operations for that reason, connector_download, which pulls a session or a
window of it off the connector (the recording volume on an Azure file share), connector_fetchFile, which pulls one of a
session's artifacts off it the same way,
operations_cancel, which asks background work to stop, and ide_link and ide_open, which act on the
editor running beside Jeffrey rather than on any profile. Each declares itself, so the reading members
of those same families - recordings_list, recordings_status, heap_status, connector_files,
operations_status - are not swept up with them. Two families
reach outside this server and have switches of their own —
jeffrey.microscope.mcp.connector.enabled=false for the one that leaves the machine, and
jeffrey.microscope.mcp.ide.enabled=false for the one that reaches into the developer's IntelliJ.
Full documentation: Microscope MCP — Claude Code, Codex, Gemini CLI, other clients.
Install
Jeffrey's MCP server is on by default — a running Jeffrey is already serving it, at
/api/mcp on whatever address and port you reach Jeffrey on (http://localhost:8585 unless
you changed server.port).
/api/mcp is the one path the server answers on.
Claude Code (on the v2 MCP runtime — MCP_SDK_GENERATION=v2 claude where it is not the default):
/plugin marketplace add petrbouda/jeffrey
/plugin install microscope@jeffrey
Codex (v0.147.0 or later, with the global feature flag [features] mcp_2026_07_28 = true):
codex plugin marketplace add petrbouda/jeffrey
then /plugins in Codex, or work from a clone with codex plugin marketplace add ./jeffrey.
Gemini CLI (installs, but cannot connect until Gemini CLI supports MCP 2026-07-28):
gemini extensions install https://github.com/petrbouda/jeffrey --ref gemini-extension
Gemini wants gemini-extension.json at the root of whatever it fetches, and on master it lives
one directory down. The gemini-extension branch is this directory at its root, without the evals,
published by CI (publish-gemini-extension.yml)
on every change to it on master; --auto-update keeps an install on it. From a clone, the
directory itself works too:
git clone https://github.com/petrbouda/jeffrey
gemini extensions install ./jeffrey/jeffrey-agent-plugin
/extensions lists it, /mcp says whether it reached Jeffrey.
Any of them can also skip the plugin and register the endpoint by hand — the docs above cover
claude mcp add, codex mcp add, Gemini's mcpServers block, and the raw 2026-07-28 requests
(server/discover, tools/call, …) for anything else.
Pointing it at your Jeffrey
The plugin ships with the endpoint set to http://localhost:8585/api/mcp.
In Claude Code anywhere else — a different port, a container, an SSH tunnel — is a setting:
Claude Code offers the field when you enable the plugin, /plugin reopens it afterwards, and the
value lives per machine in ~/.claude/settings.json.
Jeffrey MCP endpoint: http://localhost:9000/api/mcp
Codex has no equivalent. The Agent Plugins format forbids placeholder expansion in a server URL,
so a plugin-provided endpoint is fixed at localhost:8585. For any other address, disable the
plugin's server and register your own in ~/.codex/config.toml:
[mcp_servers.jeffrey]
url = "http://localhost:9000/api/mcp"
tool_timeout_sec = 120
bearer_token_env_var = "JEFFREY_MCP_TOKEN"
The skills keep working; only the server registration moves. tool_timeout_sec = 120 matters more
than it looks: Codex gives up on a tool call after 60 seconds by default, while a slow Jeffrey read
(jvm_autoAnalysis with compute, heap_oql with includeRetainedSize, a large flamegraph export)
waits up to 45 seconds before handing back an operationId and can take longer than that to render —
at 60 seconds Codex reports a timeout for a call that was about to answer. The
bearer_token_env_var line is harmless without a token; see below.
Gemini CLI has a setting, like Claude Code. The extension declares one, and Gemini asks for it
while installing; the manifest reads ${JEFFREY_MCP_ENDPOINT:-http://localhost:8585/api/mcp}, so the
default stands until you give it something. gemini extensions install with --skip-settings skips
the question, and exporting JEFFREY_MCP_ENDPOINT answers it from the environment instead — the same
variable the session-start check reads, so both agree about where Jeffrey is.
Registering the server yourself works too, in ~/.gemini/settings.json for every project or a
checkout's .gemini/settings.json for one:
{
"mcpServers": {
"jeffrey": {
"httpUrl": "http://localhost:9000/api/mcp"
}
}
}
httpUrl is one of three spellings Gemini accepts for the same server: url with "type": "http"
is identical, and is what gemini mcp add jeffrey <url> --transport http writes; a bare url works
too, because Gemini tries HTTP first and falls back to SSE only when that fails. Only "type": "sse"
is wrong here. Keep the name jeffrey: the skills name tools by the part after the prefix, and the
prefix is built from the server's name.
A token, when Jeffrey asks for one
A Jeffrey reachable from more than your machine can require a bearer token:
jeffrey.microscope.mcp.token in its application.properties. Each client then sends it as
Authorization: Bearer <token>; outside Claude Code's own setting it comes from JEFFREY_MCP_TOKEN:
- Claude Code — the plugin's second setting, Jeffrey MCP token (
/plugin→ microscope), kept in the system's secure storage and sent asAuthorization: Bearer …. Left empty, the header goes out empty, and a Jeffrey without a token ignores it. - Codex —
bearer_token_env_var = "JEFFREY_MCP_TOKEN"in the server'sconfig.tomlblock above; export the variable before starting Codex. The plugin's own fixed server sends no token, so a Jeffrey that requires one is registered by hand, like any other address. - Gemini CLI — the extension sends
Authorization: Bearer ${JEFFREY_MCP_TOKEN:-}; export the variable before starting Gemini.
The session-start check sends the token the tools send — under Claude Code only the plugin's
setting, elsewhere JEFFREY_MCP_TOKEN — and says which of three things is wrong when a session
opens in a JVM project (a Maven, Gradle, sbt or Mill build file at its root, or a recording or heap
dump near it — anywhere else it stays silent): nothing answering, a missing or mismatched token (HTTP 401 — it names where this client takes
the token from), or a refused host or origin (HTTP 403 — it quotes Jeffrey's own reason, which names
the property to change).
What you get
Tools, in nineteen families:
| Family | What it does |
|---|---|
profiles_ | The catalogue: which recordings are analysed, what each one can answer, deep links into the UI, and a versioned evidence snapshot of one profile to cite |
operations_ | The work the writers start — an import, a connector download or file fetch, a heap preparation — followed by its operationId: where it has got to, and a request that it stop |
flamegraph_ | Which graphs a profile supports, and the call tree as Markdown |
compare_ | Two profiles against each other: whether they are comparable, whether their evidence supports a verdict at all, what moved, and the differential call tree |
traces_ | Trace operations, the application's own notifications, exemplars, span trees, span-scoped flamegraphs, and the attributes that say which population a trace belonged to |
jvm_ | The machine underneath: garbage collection and the pages beneath it, safepoints, JIT compilation, threads, thread dumps, native memory, class loading, exceptions, the host and who else is on it, TLS and certificates, the container, the JVM flags and what it was started with |
http_ | The HTTP traffic the application served: percentiles, endpoints, status codes, slowest requests |
jdbc_ | Statement timings and groups, and the connection pool in front of them |
grpc_ | gRPC latency per service and method, and the message sizes moved |
methodtracing_ | Instrumented method timings (JEP 520): by cost, the worst invocations, the JVM's aggregates |
io_ | Socket and file I/O: bytes, targets and the slowest operations — the waiting a CPU graph cannot see |
blocking_ | Contended monitors, waits, parks and virtual-thread pinning |
timeline_ | When the samples landed: the busiest windows, and sub-second zoom inside one |
memory_ | Allocation by type, and JFR-side leak candidates that need no heap dump |
jfr_ | The profile's DuckDB tables — schema, the fields of one event type, and read-only SQL |
heap_ | Heap summary, class histogram, dominator tree, leak suspects, GC-root paths, a two-dump diff, read-only SQL, OQL, and the one pair that builds rather than reads: heap_prepare and heap_status |
recordings_ | One of the six families with a writer in it: imports a recording file and builds a profile from it |
connector_ | The recordings still on the connector — the recording volume on an Azure file share: lists sessions across every workspace and project, pulls one in — the whole session, the recording files covering a time window, or files named from the listing — and lists the files a session holds beside its recording (application logs, gc.jvm-log, the crash file, perf counters, a heap dump) and fetches one of them on its own, as a path the agent reads with its own tools |
ide_ | Where a frame actually lives, answered by the developer's running IntelliJ: the file and line for a class and method, a class's source, which checkouts are open and on what commit, and — the one tool here with a visible side effect — opening a location in the editor |
recordings_analyzeFile takes an absolute path, and the file has to be on the machine Jeffrey
runs on — Jeffrey opens it, the client does not upload it. That is the usual case (one laptop running
both) and not the case for a Jeffrey in a container or on another host.
Skills, which the agent picks up on its own and you can also invoke directly —
/microscope:analyze-jfr in Claude Code, $analyze-jfr in Codex, and by name in Gemini CLI, which
loads a skill when the question calls for it:
help— what Jeffrey can investigate before any profile is chosen: the areas a recording or a heap dump can answer, what each needs recorded, which skill takes it, and what is here nowanalyze-jfr— where to start and which family answers which questionprofile-run— a workload that has not been recorded yet: what to run it under, for how long, and where the file has to landanalyze-heap— a heap dump end to end: what is holding the memory, what is leaking, which class loader never went away, and the order the heap tools have to be run inanalyze-connector— the recordings that never reached this machine: finds a session on the connector, pulls it in, and hands off toanalyze-jfroranalyze-heapcompare-jfr— before against after: whether a change made it slower, which methods moved, and whether the two recordings were comparable in the first placeregression-check— the same question starting from two revisions rather than two profiles: build and record both, then weigh themadvise-jfr— from a profile to a code change: the hottest CPU, wall-clock, allocation and blocking frames mapped to real source in your checkout, a recommendation, then the edit and a re-profile on requestanalyze-exceptions— what an application threw and why: its own throws (libraries included) set apart from the JVM's class-loading and linking noise, each throw site traced to the line of your checkout that led to it, a recommendation, then the edit on requestjfr-sql— the JFR schema and the DuckDB idioms that go with itheap-sql— the heap-dump index schemareport— the shape every finding is written in, and the evidence rules behind it: every figure names its tool call, shares say what they are a share of, sampled and rule-based evidence is capped at medium confidence, and what the recording could not answer is reported apart from the findings
The exports carry their own reading instructions, so the skills stay short: they cover the
workflows and the two schemas, not things the tool output already explains. They name tools
without the prefix each client puts in front of them, and each says in one sentence what the three
prefixes are; the full table is in
skills/report/references/tool-prefixes.md. No skill
points into another's directory: a client that loads the skills from the server reads only the files
a skill lists as its own.
Two agents. profile-analyst is the general one. A single flamegraph export can run to 120,000 characters,
and a question usually takes several. The analyst runs a sequence and returns only the findings — the
hot frames with their shares, or the retaining classes with their GC-root paths — leaving everything
it read in its own context. The skills hand it the reading and keep what needs your conversation:
mapping frames onto the checkout, the recommendation, and every question put to you.
heap-triage is the heap specialist: it carries a handbook cut from analyze-heap and heap-sql, runs the whole leak
route, and will build a missing dominator tree or report with heap_prepare rather than reporting an
empty result.
An open-ended question — "why is this service slow", "review this recording" — needs no agent of its own: the skill puts the profile's menu of investigation areas to you, and sends the areas you pick to the analysts in one message so they run at once, while it can still ask you.
Claude Code gets them from the plugin as microscope:profile-analyst and microscope:heap-triage,
with no file tools and no writer that leaves Jeffrey's caches, so they
cannot touch your files, import a recording, pull one off the connector, move your editor or propose
an edit. They are not strictly read-only: jvm_autoAnalysis with compute and heap_oql with
includeRetainedSize fill a cache, and heap-triage also runs heap_prepare to build a missing
report — profile-analyst is denied that one. Agent Plugins defines only
skills and MCP servers, so Codex cannot receive an agent from a plugin — copy
codex/agents/profile-analyst.toml and
codex/agents/heap-triage.toml to ~/.codex/agents/ instead.
Those versions are sandboxed read-only, but their tool restriction is instruction-level rather than
enforced.
Gemini CLI gets both, also as files to copy —
gemini/agents/profile-analyst.md and
gemini/agents/heap-triage.md to ~/.gemini/agents/ for every
repository, or .gemini/agents/ for one; /agents then lists them. The extension cannot carry them
even though Gemini reads an extension's agents/: it validates that frontmatter strictly and rejects
any key it does not define, and the plugin's own agents carry Claude Code's disallowedTools,
skills and color.
Prompts, skills and resources. The server also serves the twelve skills over the protocol itself,
as MCP prompts and over the MCP skills extension — the same files the plugin ships, copied onto the
server's classpath when it is built, so the two cannot drift. A client that cannot install a plugin
(Cursor, VS Code, Kiro, anything registered by hand) gets them through skills/list when it speaks
2026-07-28 with the skills extension, or through prompts/list and prompts/get, rather than
being left with the tools and no account of how to use them. resources/list and resources/read expose the profile
catalogue the same way, with URI templates for one profile's summary
(jeffrey://profile/{profileId}/summary), a flamegraph of one event type
(jeffrey://profile/{profileId}/flamegraph/{eventType}), its evidence snapshot (…/evidence), its
database schema (…/schema) and the findings it already holds (…/findings, read from the cache, so
reading it never starts an analysis) — a client can pin one into the conversation instead of spending
a tool call on it.
How an answer reads. Every tool but ide_source declares an outputSchema and answers a typed
record in structuredContent: times are UTC epoch milliseconds (…EpochMs), enum values are upper
case in answers and arguments alike (bar profiles_viewLink's view, a Microscope page path), a heap objectId is a decimal string, "no data" is a status
with a reason rather than an error, a list that continues returns hasMore and a nextCursor to pass
back as cursor, and followUp.nextTools are the next calls with their arguments filled in. An answer
whose subject has a page in Microscope carries a uiLink to it — the skills hand it to the user with
the finding. Nothing is exported to a file. Each tool's _meta says what a call costs
(jeffrey/cost) and what it needs in place (jeffrey/requires).
Narrowing what is advertised. jeffrey.microscope.mcp.families takes a comma-separated
allow-list of family prefixes — profiles,flamegraph,jfr,heap,operations, say — and everything outside it is
left out of tools/list entirely. Empty, the default, advertises whatever
jeffrey.microscope.mcp.preset selects — all, or jfr, heap and connector, each of which keeps
profiles, recordings and operations beside the families it is named for. A non-empty list wins
over the preset, and operations must be in any list that keeps a writer family (recordings, heap,
connector, ide or jvm) — startup fails otherwise — since it is how the work the writer starts is followed;
likewise recordings must be in any list that keeps connector, since a downloaded session is analysed with
recordings_analyzeRecording. It is the blunt instrument for a shared installation, and it composes
with the two per-family switches above.
Permissions
Every client asks before each tool the first time. Every Jeffrey tool reads except the eleven named
above, so approving a family once is usually what you want — connector_ and ide_ are the two worth
reading twice, since one moves data off the connector and the other acts on your editor.
Claude Code, from the prompt or up front with /permissions:
mcp__plugin_microscope_jeffrey__*
Codex, in ~/.codex/config.toml:
[mcp_servers.jeffrey]
default_tools_approval_mode = "auto"
Gemini CLI keeps the same choice behind its prompt — Always allow this tool and Always allow this
server build the allow-list as you go. To decide up front instead, mark the server trusted in
settings.json, or name the tools you want without a prompt:
{
"mcpServers": {
"jeffrey": {
"httpUrl": "http://localhost:8585/api/mcp",
"trust": true
}
}
}
trust covers every tool on the server, ide_ and connector_download included, so it is the blunt
instrument; excludeTools on the same entry is how you keep those two out of reach entirely.
Try it
With a profile analysed in Jeffrey:
list the Jeffrey profiles, then show me where the CPU time goes in the most recent one
Or, starting from a recording that is not in Jeffrey yet:
analyze target/checkout-run.jfr in Jeffrey and tell me which of my methods dominate the profile
the
GET /api/ordersoperation is slow — find a slow example and tell me what the JVM was doing inside its slowest span
Or, from a heap dump:
analyze /tmp/heap.hprof in Jeffrey — what is holding the memory, and is anything leaking?
Or, once the hotspot is known, in the repository that produced it:
advise on the most recent Jeffrey profile — what should I change in this repo?
Package layout
jeffrey-agent-plugin/
├── plugin.json Agent Plugins 1.0.0 manifest — Cursor, Copilot, VS Code, Kiro
├── mcp.json Agent Plugins MCP config — the streamable-http endpoint
├── .claude-plugin/
│ └── plugin.json Claude Code manifest — same plugin, with the configurable endpoint
├── .codex-plugin/
│ └── plugin.json Codex-native manifest, pointing at the same skills and mcp.json
├── gemini-extension.json Gemini CLI extension manifest — the same server, skills and agents
├── hooks/ SessionStart check — is Jeffrey actually serving (Claude Code and Gemini)
├── skills/ Twelve skills, read by all three formats
├── agents/
│ ├── profile-analyst.md Claude Code subagent
│ └── heap-triage.md …and the heap specialist
├── codex/agents/
│ ├── profile-analyst.toml The same agents as Codex custom agents, installed by hand
│ └── heap-triage.toml
├── gemini/agents/
│ ├── profile-analyst.md The same two as Gemini CLI subagents, also installed by hand
│ └── heap-triage.md
├── evals/ Behaviour evals against recorded Jeffrey answers (see evals/README.md)
└── evals-live/ The SQL cases, run against a live Jeffrey
One package for every coding agent. The directory is named for no single client: it is the one
package Claude Code, Codex, Gemini CLI and the Agent Plugins clients install, and both marketplaces
point at it. A client keys an install by the plugin's name — microscope@jeffrey — never by this
path, so moving the directory (it was jeffrey-claude-plugin/ until 1.0.29) left every marketplace
install in place; only a --plugin-dir or a Gemini install from a clone names the path itself.
The evals ship with it. evals/ and evals-live/ are development material, yet they are in
every installed copy: claude plugin eval reads an eval directory only from below the plugin root,
never from beside it. The gemini-extension branch leaves them out.
One hook, two clients. Both read hooks/hooks.json in the same shape and both spell a hook's
output the same way, but they name the plugin's own directory differently — CLAUDE_PLUGIN_ROOT in
the environment, ${extensionPath} substituted into the command — so the command resolves whichever
is there and exits silently when neither is, rather than reporting a missing script. It carries no
timeout on purpose: Claude Code reads that field as seconds and Gemini as milliseconds, and the
probe bounds itself at three seconds anyway.
Three agent directories, one set of agents. agents/ is Claude Code's, and the only one a plugin
carries; codex/ and gemini/ hold the same analyst and heap specialist in the dialect each of those
clients validates, to be copied by hand. Gemini does read an installed extension's agents/, and
prints a validation error for each of the two Claude files, whose frontmatter its strict schema
rejects — on ordinary commands, not only in debug mode. Nothing else follows from it: the extension
loads, the server connects and the skills work. It is the cost of one directory that has to satisfy
Claude Code, whose plugins read agents/ and nothing else.
Written once, rendered three times. None of the agent files, and none of the skill sections every
skill shares (Link what you name, the tool-prefix sentence), is edited where it ships: each has one
source under build/plugin/, and build/scripts/render-plugin.py writes the
copies — the Claude Code subagent with its tool lists spelled out from the server's tool snapshot,
and the Codex and Gemini versions with the text only those clients need. The four manifests and the
plugin entry of both marketplaces are rendered the same way, from build/plugin/manifests/: the
name, version, description and keywords once in identity.json — a version bump is that one edit
and a run of the script — and one template per manifest for what only that client reads. CI runs it
with --check.
In both hand-copied sets the read-only promise rests on the No writing rule in the agent's own instructions rather than on a deny-list the client does not have.
Security
The endpoint refuses a request carrying an Origin header it did not serve, which is the check the
MCP specification asks of a local HTTP server — a CLI client sends no Origin, so it costs nothing
here and closes the path where a page in your browser drives the server.
It also refuses a Host outside jeffrey.microscope.mcp.allowed-hosts (loopback by default), with a
403 that names the property: reaching Jeffrey from a container as host.docker.internal, or through a
reverse proxy, means adding that name. Behind a proxy that sets X-Forwarded-Host and
X-Forwarded-Proto, jeffrey.microscope.mcp.trust-forwarded-headers=true makes the guard judge the
address the client used rather than the proxy's — only when the proxy is the one way in, since anyone
who reaches Jeffrey directly can write those headers.
Without jeffrey.microscope.mcp.token it is unauthenticated, exactly like the rest of Jeffrey's API:
anyone who can reach the address can read every profile in that installation. Keep it bound to
localhost with server.address=127.0.0.1, put it behind an SSH tunnel or an authenticating reverse
proxy, or set the token (above) so every request has to present it.
Licence
Apache-2.0, as the rest of Jeffrey. See LICENSE.