Guide
Connect your agent
The agent you already work with can ask what your app is doing in live. It connects to a hosted Model Context Protocol server with a read-only key you mint and revoke, and the tools it finds there answer from your app's own data — the same registry the console reads, so the two can never disagree about what a number means. Nothing is added to your app. (A connection that presents your control credential instead can also do what the console does for you — move an issue as you, and read and change your remote configuration; the tools that need it say so below.)
1. Mint a read-only key
A query key is scoped to the apps and the one environment you name. Organization and environment derive from the key itself, never from the request; a foreign app is refused. For a new integration, use the app id and environment shown by the console’s Integration status page or the URL printed by pharen verify --wait. The example below uses live; use your Debug environment instead when checking a local run.
pharen query-keys create --environment live --app your-app
# → phq_live_… shown once — keep it with the agent that uses itOmit --app for an organization-wide key that reads every app you own, including apps you onboard later. Rotation and revocation are on Keys & credentials.
2. Connect
The server speaks Streamable HTTP at https://mcp.pharen.ai/mcp and authenticates every call with the key in an Authorization header. Replace the example key with the one you just minted, including a test key for a test environment. The connection command does not select an environment; the key does. Any client that can send that header can connect.
Claude Code
claude mcp add pharen --transport http https://mcp.pharen.ai/mcp \
--header "Authorization: Bearer phq_live_…"Codex
codex mcp add pharen --url https://mcp.pharen.ai/mcp \
--bearer-token-env-var PHAREN_QUERY_KEYCodex reads the key from that variable each time it starts, so export it in your shell profile: export PHAREN_QUERY_KEY=phq_live_…. The same server, as configuration:
[mcp_servers.pharen]
url = "https://mcp.pharen.ai/mcp"
bearer_token_env_var = "PHAREN_QUERY_KEY"Cursor
{
"mcpServers": {
"pharen": {
"url": "https://mcp.pharen.ai/mcp",
"headers": { "Authorization": "Bearer phq_live_…" }
}
}
}Any other MCP client
transport Streamable HTTP
url https://mcp.pharen.ai/mcp
header Authorization: Bearer phq_live_…The host is a thin transport: it keeps no session and no key, forwards yours on every call, and adds no privilege of its own. A missing key is refused before any tool runs. A client that has no way to send a header cannot connect yet — tell us which one.
3. Ask
Start with your first local run: call pharen_list_metrics and pharen_apps, then ask for the ungrouped events.count total for your app in the key’s environment. This includes local Debug runs and needs no additional grants. Verify the specific build’s first receipt with Integration status or pharen verify --wait. Grouped or filtered counts can be withheld when small, which is not zero. Check the catalog before adding filters: events.count has no release_track filter.
The first call an agent should make is pharen_list_metrics: the live catalog, with each metric's meaning, its dimensions and its filters. From there, questions like these compose into one or two tool calls:
Crash health
- What is the top issue on the latest build, and where in the code does it crash?
- Which app_version and device_model drive crashes.count this week?
- Did crashes.count move with sessions.count after the latest release?
- Show crashes.count by day for the last 30 days, released builds only.
Reach and adoption
- How many distinct installs are on version 9.9.9 in live?
- What share of last week’s new installs came back this week?
- Which OS versions is the install base on, read against platform?
The store
- Did the store rating move after 9.9.9 shipped?
- Which reviews are still unanswered, and what do they say?
- Where does the app rank in its category chart this week?
Configuration
- Which builds have applied the current remote configuration, and how many installs are behind each?
- What does build 740 actually enforce right now?
The tools
A reference row states what a shipped tool does. The tools that need your control credential hit the same routes the CLI and console use; a query-key connection calling them gets the platform's refusal verbatim. One of them writes: pharen_issue_transition acts as the person who signed in, so it takes the session pharen auth login stores and refuses a CI job's credential.
| Tool | Answers | Needs |
|---|---|---|
pharen_repair_plan | Plan one bounded Apple store recheck against the current connection and ongoing credential. Planning stores a five-minute record without scheduling work; apply is disabled for every organization until explicitly enabled. | person control session |
pharen_repair_apply | Local stdio only, explicitly enabled: apply the exact plan after human authorization, with scope and stale-state checks, replay protection and backoff. Scheduling is not proof of imported analytics. Remote MCP cannot apply. | local person control session; organization enablement required |
pharen_list_metrics | The live catalog: every metric, what it means, and which dimensions and filters it accepts. The first call an agent should make. | query key |
pharen_apps | The apps this connection may ask about, by name and by id — the owner’s name for each, the platforms it has shipped for, and whether it is retired. The other half of the catalog: for a question that names an app in words, this is what turns the name into the identifier every other tool takes. | query key |
pharen_query | One registry-validated aggregate — a metric, grouped and filtered by registry dimensions, over a time range. Grouped or filtered rows below the small-count threshold are withheld for builds that may have reached people you never named. | query key |
pharen_issues | Your issues, newest activity first: crashes grouped by cause — title, exception, status, how many crashes, when it was first and last seen, the newest build seen, and the frames of one representative crash — and beside them the reports people filed, each as its number, its status and the word it was asked as. The words of a report ride each row only for a key minted with the report_text grant, or your own sign-in, as a masked copy with a manifest of what was replaced; every other key is told the row has words and that they are withheld. Never a single crash row, an identifier, or an original. | query key |
pharen_issue | One issue by its number or id, in the same shape as a list row, with the report behind it: its category, the build and platform it came from, which key it rode in on, and which parts the device meant to attach. An owner or a developer connected with their own sign-in may ask for the reporter’s words unmasked; that read is recorded on the issue’s history, and any other connection is refused rather than handed the masked copy. | query key |
pharen_user | One user, by the reference the platform gave them: when they were first and last seen, each install they signed in on with its lifecycle and push reachability, their recent sessions, and the issues they hit and the reports they filed — plus the values your app supplied about them that the key’s identity grants cover, each labeled with its grant. Every read is recorded in your user lookup log. | query key with the users grant |
pharen_user_lookup | The users one exact value finds — a reference, an install id, an email address, a phone number in international form, a name as whole words, a username, your app’s user id or a group id — each kind under its own grant beside users. Never fuzzy; the value travels in the request body, and every lookup is recorded with a keyed hash of it, never the value. | query key with the users grant, and the grant the kind names |
pharen_issue_transition | One lifecycle verb — resolve, ignore, reopen, amend, assign to yourself, unassign, start, merge — sent as the person who signed in, with the status the caller last saw so a teammate’s change is refused by name rather than overwritten. A query key cannot write, and neither can a CI job’s credential; the refusal comes back verbatim. | control credential — your own sign-in |
pharen_store_overview | What the store measures from outside: ratings per territory, search and chart rank, downloads and engagement, the store’s own launch and hang percentiles, a short recent-reviews preview, and the sync state every number depends on. | query key |
pharen_store_reviews | Customer reviews as published on the store — rating, title, body, the reviewer’s public nickname, any developer response — newest first, filterable by territory, rating and answered state. | query key |
pharen_store_series | One daily series for one store metric: downloads and their components, impressions, product-page views, active devices, ratings, search or chart rank. Absent days are null, never zero. | query key |
pharen_get_effective_config | What each build that has reported actually enforces: its compiled configuration with the current remote configuration applied over it, computed on the server. | control credential |
pharen_get_rating_policy | The app-rating prompt policy and prompt-budget sections of your remote configuration, with recent revision history. | control credential |
pharen_set_rating_policy | Writes those two sections and nothing else; the note you supply becomes the revision’s recorded reason. | control credential |
Reading one user
Counts answer most questions, and a key reads nobody until you say otherwise. A key minted with the users grant reads users: pharen_user opens one user and pharen_user_lookup finds users by one exact value, and pharen_query may narrow a metric to one user, install or session. What your app said about them rides three more grants, each minted beside users: identity_contact (an email address, a phone number), identity_name (the name parts, a username) and identity_traits (the user id your app supplied, the group id and the rest). A value the key's grants do not cover is named as withheld, never passed off as missing.
Only an owner or a developer who holds identity read may mint such a key, and it carries the reason it was minted. Every read of a user it makes is written to your user lookup log — which user, which key, when; for a lookup, a keyed hash of the value rather than the value — and the console shows that log to your owners and developers. A reporter's address, read under report_contact, is recorded on the issue's history instead.
Reading the words people wrote
A query key reads counts, your issues and what the store shows. It does not read what a person typed into a report unless you mint it with the grant:
pharen query-keys create --environment live --app your-app \
--grant report_textThe command prints a warning before the key is shown, and the warning is the rule: the key reads the words people wrote in their reports for the apps it names; addresses and phone numbers in them are replaced; whatever holds the key can read them, and if that is an AI service, your own terms with that service apply to what it reads; so scope the key to one app, label it with the agent it serves, and revoke it when that agent is retired. Only an owner or a developer can mint one, and rotation carries the grant to the replacement. pharen query-keys list and the console's API keys page show which keys carry it.
What the agent is served
Every issue row carries four fields: words_present, words_withheld, words and mask. On a key with the grant — or on your own sign-in — words is a masked copy: an address or phone number someone typed is replaced by a bracketed token, so is a bare run of ten or more digits, and a link keeps its host and path while its query string is removed and marked and identifier-shaped segments are templated. mask.replaced counts what was touched, so the agent can say an address was replaced rather than read a token as text; an empty manifest means no identifier was replaced. A key without the grant is told the row has words and that they are withheld, and reads no text. The list never serves an original.
What never crosses
The picture a reporter attached and a survey answer reach no key, granted or not. And never a raw event, a location a device shared, anything from the special-category store, anything under the health purpose.
The address a reporter left
A reporter may leave an address to be reached at. A key minted with the report_contact grant is served it on pharen_issue, with where it came from and when; every other connection is told it is withheld. Each read that serves one is written to the issue's history as a line naming the key and the instant — never the address — so your team sees who reached for it, exactly as it sees who resolved the issue. Reach the person through your own channel.
The original, on the record
An owner or a developer connected with their own sign-in — the session pharen auth login stores, sent in the same header a key rides in — may ask pharen_issue for the words unmasked (original: true). That read is written to the issue's history as a line naming who read them and when, beside who resolved it, and the console shows it. A key asking is refused, and so is a reader's sign-in. See Keys & credentials for the key's whole lifecycle.
What is refused, and why
- A metric, dimension or filter that is not in the registry — refused with a stable code the agent can read and correct. There is no free-form query language.
- An app the key does not name, or an environment other than the key's own — refused, never silently widened.
- A grouped or filtered cell below the small-count threshold, for builds that may have reached people you never named — withheld and counted in
meta.withheld_rows, so an empty answer there means fewer than the threshold, never zero. A filter narrows exactly as a group-by does. Three release tracks are withheld:released,testing(a TestFlight group or public link, an invite link, a Play track you declared testing) andunconfirmed(nothing establishes where the build went). Your own builds answer exactly:developer(debug, simulator and development-signed builds) andinternal_testing(ad-hoc builds on devices you registered one by one), when the query names the track (arelease_trackfilter or group-by) on a metric that offers one; a query that cannot prove the track, on any other metric included, is withheld like a customer cell. TestFlight builds, store builds and tester builds that name no supplier are the exception: a TestFlight public link admits testers you never named, so a cell holding any of them is withheld whatever its track. The floor applies to each cell on its own: subtracting exact cells from an exact total across queries can recover a withheld one. A total narrowed by nothing is exact at any size, and a key minted with--grant small_countsreads every cell exactly.
Two caveats an agent gets wrong
environment is the environment the key belongs to, not a statement about the build. A developer's local install pointed at the live ingest key emits environment=live. The metrics that describe customers — sessions.count, installs.seen, crashes.count and custom_events.count — default to release_track=released, the builds that reached customers; events.count does not.
release_track=unconfirmed means “cannot tell”, not zero. A released count of 0 beside a large unconfirmed count says nothing established where those builds ran; declaring a build released (pharen releases create --release-track released) moves its rows. Group by release_track before concluding anything from a zero.
Old names. The filter and group-by are release_track, not provenance; the build's distribution class is distribution, not channel; the grant is small_counts, not small_cells. A request that sends an old name is refused, never answered under a different meaning.