Skip to content

Add read-only Fathom Analytics MCP - #230

Open
9ranty wants to merge 1243 commits into
mainfrom
grant/fathom-mcp
Open

9ranty wants to merge 1243 commits into
mainfrom
grant/fathom-mcp

Conversation

@9ranty

@9ranty 9ranty commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Summary

  • add a first-party local stdio MCP for pinned-site, GET-only Fathom analytics
  • expose bounded site, summary, breakdown, and current visitor tools with deterministic comparisons
  • add mocked contract tests plus secure per-user setup documentation

Security

  • credentials are projected only through the external MCP env and never committed
  • FATHOM_SITE_ID is pinned; schemas and fixed endpoint enums reject arbitrary API access
  • retries, timeouts, caching, numeric parsing, output caps, and error redaction are bounded

Tests

  • bun test ./scripts/mcp-fathom.test.ts
  • focused TypeScript check for the two Fathom scripts

Started by Grant Shaddick in this OS session

Created by this OS session

@vercel

vercel Bot commented Aug 25, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
opensession Ready Ready Preview Sep 3, 2026 1:38pm UTC

@open-session-os-tella-dev

open-session-os-tella-dev Bot commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

🤖 OS review · request changes · confidence 2/5

Safe once the two P1 issues below are fixed. The MCP is appropriately constrained to pinned-site GET requests, but it is unavailable in the default compiled installation and can report incorrect breakdown comparisons when a prior row falls outside the requested top-N.
2 inline comments below.

💡 Labels: os-auto-fix — I fix these and push until CI passes · os-adversarial — deeper two-pass review · os-simplify — quality cleanup pass.
Reviewed c7b5fb8 · GPT-5.6 Sol · earlier reviews collapse above · open session

🔁 Not merge-ready and no live session owns this branch — add the os-auto-fix label and I'll fix the findings automatically.

@open-session-os-tella-dev open-session-os-tella-dev Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OS review · c7b5fb8

Comment thread mcp-config.example.json Outdated
"args": [
"run",
"/home/ubuntu/projects/opensession/scripts/mcp-fathom.ts"
],

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 P1 — Ship an executable MCP path for compiled installations

This path only exists in a source checkout, while the default installation is the compiled release. scripts/build-compile.ts documents that the release archive contains the executable, native/worker sidecars, service files, deploy files, and release.json, and install.sh explicitly detects compiled releases by their lack of a scripts/ directory and does not install Bun. Consequently, a default-install user following this configuration has neither this TypeScript file nor necessarily a bun command, so MCP startup fails with ENOENT. Expose the server through a compiled-binary subcommand such as opensession fathom-mcp, or package a standalone executable sidecar and configure its installed path; keep the TypeScript invocation only as the source-install alternative.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in de0fb3e: added an opensession fathom-mcp CLI entrypoint that is bundled into compiled releases, and changed the example and setup docs to use that stable installed command.

Comment thread scripts/mcp-fathom.ts Outdated
this.getSite(), this.aggregation(range, fields), prior ? this.aggregation(prior, fields) : Promise.resolve(null),
]);
const current = normalizeRanking(currentRaw, field, metric, limit);
const previous = priorRaw ? normalizeRanking(priorRaw, field, metric, limit) : [];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 P1 — Do not treat absence from the prior top-N as zero traffic

Both aggregation requests are capped to limit, but a current row missing from the prior response is assigned prior_value: 0 and passed to delta, which labels it new. For example, with limit: 10, a page that moves from rank 11 with 90 visits to rank 10 with 100 visits is absent from the prior top ten, so this returns prior_value: 0, absolute: 100, and status: "new" instead of acknowledging that the prior value is unknown. This makes the comparison materially false. Represent an unmatched prior row with prior_value: null and pass null to delta so it becomes unavailable, or fetch enough prior data to establish the actual value and rank.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in de0fb3e: unmatched prior top-N rows now return null prior values and unavailable deltas instead of claiming a zero baseline or new traffic. The focused comparison test covers this case.

jfrolich and others added 26 commits August 31, 2026 10:13
A mention only clears when you open the exact session it was recorded on,
but the workspace row's badge can sit on a row whose click opens a
different sibling — so the badge could never be cleared from where it
appears. Carry the mentioned member's id on the row and make the badge a
click target that opens that session (clearing the mention on open).
…e006

Commit 1b4e006 swept in the Sidebar.tsx side of another session's
subagent-grouping refactor (activeSubagentsForWorkspace ->
activeSubagentsByWorkspace) while the new export stayed uncommitted in
the shared checkout, leaving main unbuildable. This lands the matching
lib half and its tests, both authored by that session, to restore a
buildable main.
kentdebruin and others added 29 commits September 2, 2026 13:27
Vendor and enable the anti-slop Oxlint rules, resolve existing frontend findings, and expose the shared Electron bridge contract to all frontend builds.
…ends

The WebSocket command outbox kept every pending command and tombstone
forever. Once an Electron profile had collected enough of them, put()
returned false and every mutation, including Create, failed with a
generic message and no way to recover after the Reliability tab went.

- tryPut() reports unavailable, full, or blocked; the composer and the
  reconnect toast say which and where to fix it.
- Pending commands expire after 7 days; tombstones after 7 days, with
  shipped untimestamped ones stamped on first sight.
- Preferences shows a Pending sends card with Retry, Forget and Forget all.
A long paste collapses into a "Pasted text" chip, but on send the block
was placed ahead of the typed message, so the instruction ended up
buried under the material it was about. The message now leads and each
pasted block follows it as

    ---

    Pasted text:

    <block>

so the bubble shows a visible split and the model sees where the
instruction stops. A paste sent on its own still goes out bare.
Live SDK conversations report each pi step from the usage on that
step's own assistant message and stream events, keyed by Anthropic
message id so the SDK's per-content-block repeats count once and an
internal ToolSearch round trip is summed.

The previous cumulative diff assumed the SDK result usage was a running
total across tool steps. It is not: its input and cache fields describe
the last request while output spans the whole turn, and the synthetic
boundary result carries no usage at all. That wiped the baseline at
every tool boundary, so the final step of a multi-step turn re-billed
the earlier steps' output and boundary audit rows logged zero tokens.

Started by Jaap Frolich in this OS session:
https://os.tella.dev/session/os-01a05ed5-6c40-704a-81e6-69e597c66e12
The Socket Mode client was deleted in 966357f but the setup UI kept
offering it as the default. SlackAgent only registers the HTTP routes,
never reads SLACK_APP_TOKEN, and verifySlackSignature fails closed on an
empty secret, so a Socket Mode save without a signing secret produced an
app whose every POST got a 401.

Setup now only describes what the server does: the transport toggle and
xapp field are gone, the manifest always carries both request URLs with
socket_mode_enabled false, SLACK_SIGNING_SECRET is unconditionally
required, and a loopback webhook base shows a warning pointing at
Domains and ingress instead of steering people to a client that is not
there. The requiredWhen hook only existed for this case, so it and
envRequired go too. A leftover SLACK_APP_TOKEN in the env file is left
alone and ignored.

Fixes #283

Co-authored-by: Michiel Westerbeek <happylinks@gmail.com>
A compiled `opensession server` runs opensession.ts, which binds
127.0.0.1:PORT through Bun.serve and never reads LISTEN_FDS; no ingress
process ships in the release either. Rendering Requires=opensession.socket
for it failed on the missing template in 0.4.52 to 0.4.55, and a socket
copied in by hand took the port so the server crash-looped on EADDRINUSE.

Compiled installs now render a socket-free unit that binds directly, the
installer removes a stale socket unit from earlier releases or the manual
workaround, start/stop/uninstall only touch the socket when its file
exists, and the socket template leaves the release artefact.

Closes #297.

Co-authored-by: Michiel Westerbeek <happylinks@gmail.com>
A large paste used to be concatenated into the message string. It now travels
as `pastedTexts` beside the prompt on every send path (REST prompt and create,
the prompt, interrupt_prompt and create_session frames). The server folds each
block into the model-visible prompt after the message inside a <pasted-text>
fence at intake, so the queue, the steer channel, persistence and search keep
carrying one string, and classifyEntry lifts the blocks back onto the entry as
`pastedTexts` on the way to every client. The queue projection and the
take-back path lift the same way, so a queued message shows its text and an
edit restores the chips.

The web bubble renders a card per paste that opens in place, with the clamped
tail fetched from the full-entry endpoint; the New session palette gains the
same chip as a session composer; a note folds the paste behind a divider since
it has no attachment slot. The native app renders the cards and the Chrome
side panel names them.
A message waiting in the queue, or held while a workspace sets up, showed only
its typed text, so a paste sent beside it looked lost until the turn started.
The queued row now ends with "· Pasted text +60 lines".
Settings → Providers can add a SuperGrok / X Premium account by device code, as a third pool beside Claude and ChatGPT with the same capabilities: ownership, pins, sideline-on-quota, designated list (bridge.xaiAccounts), rotation-proof sandbox copies (Docker read-only mount, scoped remote upload with refresh refusal and a host upkeep tick), usage meters, and the native app's third pool. Requests route through cli-chat-proxy.grok.com so they draw on the subscription's quota; the API-key xai provider is untouched and the runner loads no Pi extensions.

Closes #276.

Started by Michiel Westerbeek in this OS session: https://os.tella.dev/session/os-01a062ed-eae1-7e93-9587-090f02c052e1
Settings writes the env file through a .bak-<n> copy and an atomic .tmp
rename, both created beside it, so the gateway needs write access to the
directory, not just the file. A system-scope install that points
OPENSESSION_ENV_FILE into /etc/opensession passes installation and then
500s on every integration save with a raw EACCES, because three root-run
installers reset that directory to root:root 0700 on purpose: it holds the
executor and session-kernel credentials.

Catch the problem where it is cheapest: `service install --system` and
deploy/deploy.sh now refuse an env file whose directory the service user
cannot write, with the fix in the message. The server wraps a permission
failure in EnvFileWriteError so the integrations route returns the same
guidance instead of Bun's default error page, and the docs say where the
file may live.

Fixes #282
⌘⇧C already copies the Open Session link to what is open (session, workspace, or review). ⌘⇧G is its GitHub counterpart: the same target ⌘G opens, copied instead. The PR chip's context menu shows both chords, and the command palette gains a matching "Copy pull request link" action.
A message with more than six images failed inside delivery with a plain
Error, which the prompt route turned into a 500. The browser outbox reads
5xx as transient, so the message retried every 30 seconds with no Retry,
Edit or Discard control, never reached the agent, and could not be removed
from the queue view. Later messages still drained around it.

Share the cap through the protocol package. The route now answers 400
before delivery for a list over the cap, staging errors carry their status,
and a non-retryable kernel replay of a recorded failure answers 409, so the
outbox parks the message as failed with Edit and Discard. The composer
stops at the cap while attaching and names what it left out, and a message
brought back for editing with more is refused at send with a toast.

Co-authored-by: Grant Shaddick <grant@tella.com>
The Claude Agent SDK sends the whole custom system prompt as one
prompt-cache block, so any session-specific byte in it costs every new
session a full cache write of the ~25k-token system+tools prefix instead
of a read. Three things varied per session: the session link, requester
and GitHub login in the PR attribution section, pi's trailing "Current
working directory" line, and the absolute worktree paths pi stamps into
project_instructions and skill locations (223 of the last 247 pi sessions
ran in their own worktree).

buildRunInstructions now emits only per-repo text and points at "the
session context" for attribution. buildSessionContext carries the link,
working directory, PR footer and assignee rule, fenced as a new "session"
context source ahead of every prompt so it survives compaction and resume
misses. assembleRunSystemPrompt strips pi's cwd line and makes the
structural paths cwd-relative, which a new prompt section explains.
The tellahq installation hit its hourly GraphQL limit about once a day.
Nearly all of the spend was pr-info's `gh pr view <branch>`: gh resolves
a branch by wrapping the whole field set in `pullRequests(headRefName:,
first: 30)`, and with `commits` (100 x 100 authors) GitHub prices that at
32 points. The same fields by number cost 1. CI webhook bursts made it
worse: every check_run, status and workflow_run delivery dropped the
detail cache and broadcast pr_updated, so open tabs re-read the PR once
per delivery for as long as a pipeline ran.

- pr-info: resolve the branch to a number first (bulk cache, then a memo
  seeded from the durable snapshot, then a one-point lookup) and view by
  number. A memoized number is verified against headRefName once.
- pr-webhook: fold CI deliveries per branch into one refresh per 30s
  window, invalidating at broadcast time. PR, review, comment and push
  deliveries still refresh promptly.
- github-budget: report the bucket as consumers' own GraphQL responses
  see it (rateLimit is free to request), next to a probe that resolves
  its token through the same service credential path. The old probe
  logged used=0 through every exhaustion.
A chip's text is folded into the prompt verbatim, so pasting a few
megabytes put the whole block in context and the turn failed before the
model saw a word of it: "Prompt is too long" on a single exchange, which
compaction cannot shorten.

Past 200k characters (about 50k tokens) the web composer now stages the
paste as pasted-text.txt through the same upload path a dropped file
takes. The agent gets the usual attached-file note and reads it with its
tools; the transcript shows the file chip and a download. Below the line
a paste still collapses into a chip, and a team note keeps the chip at
any size because it has no file channel.

Started by Michiel Westerbeek in this OS session:
https://os.tella.dev/session/os-01a0671c-d75e-79c1-bd39-3f19f3713358
A distill timer used to wait in-handler on a process-wide serial queue,
holding one of the shared kernel timer slots for the whole wait plus the
model call. Many sessions going idle together could occupy every slot and
delay scheduled prompts, agent waits, and ask escalations. The handler now
tries to take the single distill slot; when it is taken it replaces its own
timer 15-30s out and returns immediately.
Local main fell 20 commits behind origin/main because git pull --ff-only
refuses the shared checkout as soon as one dirty file overlaps an upstream
commit, and nobody may discard another session's edit. Sessions then
rebased only their own commit or committed from a throwaway worktree, and
the shared branch drifted for everyone; 44 of the 61 blocking files were
byte-identical to origin/main, residue of edits that had already landed.

scripts/shared-checkout-sync.ts fast-forwards main in place: clean paths
follow upstream, edits that already landed become clean, genuine edits are
three-way merged onto the new base (index and worktree separately, with a
pre-merge copy under .git/shared-checkout-sync/), conflicts are left
untouched and listed, and the ref moves last by compare-and-swap. AGENTS.md
now makes it the only sanctioned way to move main, requires it at task start
and after every push, and forbids the worktree and rebase-only workarounds.
The skill is now written against the pinned effect@4.0.0-rc.112 rather
than the early v4 beta the upstream (joelhooks/effectts-skills, MIT)
targeted, and it carries an Open Session-specific reference for the
frontend lifecycle runtimes. Allowlist it next to the other tracked
skills and ignore .agent-sources/, the source mirror it asks agents to
clone for API lookups.
Add the hosted Braintrust MCP (https://api.braintrust.dev/mcp, bearer API
key header) to mcp-config.example.json and the MCP-only integrations
section of the setup docs.

Started by Michiel Westerbeek in this OS session:
https://os.tella.dev/session/os-01a06762-a6ea-70a4-a59c-c70fc533c2b1
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants