The data exists. Nobody assembles it.
Teams argue about output from vibes. Momentum assembles GitHub, ADO, and Cursor activity into one view, with one shared tool registry for chat and MCP.
- engineering
- dashboard
- mcp
- ai-sdk
- nextjs
- open-source
John Ryan Cottam 5 min read
The data exists. Nobody assembles it.
Every engineering team has a vibe about how productive they are. Some weeks feel fast. Some feel like nothing landed. But when someone asks “how much did we actually ship this quarter?” the answer is usually a shrug, a Jira query, or a manager’s best guess.
The data exists. Commits, PRs, lines changed, cycle times, AI token spend. It’s all sitting in GitHub, Azure DevOps, and Cursor. Nobody assembles it because it’s spread across four tabs and three logins. So the conversation about output stays subjective.
What if it didn’t have to be?
Momentum
I built a tool called Momentum, an engineering activity dashboard that aggregates GitHub, Azure DevOps, and optional Cursor analytics behind Supabase auth. One page, one date picker, five presets: This Sprint, Last Sprint, 30 Days, 60 Days, YTD.
It’s not a project management tool, a ticketing system, or a performance evaluation platform. It watches what already happened and surfaces it. It doesn’t plan, assign, or enforce anything.

- Commits and PRs across GitHub orgs and Azure DevOps, with per-contributor breakdowns
- Lines changed: additions, deletions, net. Useful for spotting build vs. refactor weeks, not for ranking people
- PR cycle time: opened to merged. Shows where reviews are fast and where they bottleneck
- Cursor analytics (optional): adoption by edit type and token spend by user and model, when your org has Cursor connected
- Ask Momentum: a chat interface that answers any of the above in plain English
- MCP: the same metrics and tools exposed for Cursor and Claude Desktop
That last pair is the part that changes how teams use it. Instead of scanning tables, you ask: “Where are PRs waiting on review?” or “Write the engineering report for last sprint.”
Ask Momentum calls the right tools, assembles the data, and returns a formatted answer with the numbers to back it up.
What it does not measure
Momentum tracks activity proxies, not outcomes. It will not tell you whether customers are happier, whether the release was stable, or whether the code is maintainable. Lines changed can spike from a deletion pass, a generated migration, or a bot. High commit counts can mean small, healthy slices, or thrash.
Use it to answer inspectable questions: Did anything merge this sprint? Where is review latency? Are we spending on models nobody uses? Not to crown a “top shipper” or replace judgment about what actually mattered.
How it works under the hood
The interesting part isn’t the dashboard. Three files do all the heavy lifting.
One cached corpus. A single year-to-date pull of GitHub, ADO, Cursor, and token data, cached for an hour. The dashboard, the chat, and the MCP server all slice from the same in-memory snapshot. Your APIs get hit once per hour, not once per question.
export const getRawCorpus = async (): Promise<RawCorpus> => {
"use cache"
cacheLife("hours")
cacheTag("dashboard-data")
const ytd = RANGE_PRESETS.ytd.resolve()
const [github, ado, cursorEdits, userContributor, tokenUsageRows] =
await Promise.all([
fetchGitHubData(ytd.since, ytd.until).catch(() => emptyGitHub),
fetchAdoData(ytd.since, ytd.until),
getLatestCursorEdits(),
getLatestUserContributor(),
loadTokenUsageRows(),
])
return { since: ytd.since, until: ytd.until, github, ado, cursorEdits, userContributor, tokenUsageRows }
}
One tool registry. Twelve tool() definitions: get_team_summary, list_members, get_pr_stats, get_token_usage, get_cursor_edits, and so on. Each takes a Zod schema and returns structured JSON.
export const momentumTools = {
get_team_summary: tool({
description: "Top-line metrics for a date window: commits, contributors, lines, GitHub vs ADO split.",
inputSchema: windowSchema,
execute: async ({ since, until }) => getTeamSummary(since, until),
}),
// ...11 more
}
Two transports, same registry. The chat route hands momentumTools to streamText. The MCP route hands the same object to createMcpHandler. That’s the entire integration:
// /api/chat
const result = streamText({
model: CHAT_MODEL,
system: MOMENTUM_SYSTEM_PROMPT,
messages: await convertToModelMessages(messages),
tools: momentumTools,
stopWhen: stepCountIs(8),
})
// /api/mcp
const mcpHandler = createMcpHandler((server) => {
for (const [name, tool] of Object.entries(momentumTools)) {
server.registerTool(name, { description: tool.description, inputSchema: tool.inputSchema }, ...)
}
})
Adding a thirteenth metric is a one-file change. Define the tool, ship it, and it shows up in both the chat and the MCP endpoint. No duplicate definitions, no drift.
Most public examples wire AI SDK chat tools and MCP tools as separate codepaths. They don’t have to be. This shared-registry pattern is the cleanest seam I’ve built in a while.
What it takes to run this on your team
Today the org-specific bits are a handful of small files, not magic. Just config. authors.ts holds identity aliases (matching the same person across GitHub and ADO), display names, and bot exclusions. GitHub orgs and the ADO project live in env vars. Cursor is optional: Cursor Enterprise customers can connect via the Cursor API; everyone else runs GitHub and ADO only.
The open-source release will fold that into a single momentum.config.ts: data sources on/off, roster, terminology (“commits” vs “changesets”), and whether Ask Momentum is enabled. Until then, the pattern is the same: pin employer-specific names to one place so the rest of the app stays generic.
The infrastructure is pluggable: Supabase auth, Vercel Blob storage, AI Gateway, MCP. Standard tools any team running on Vercel already has access to.
Why this matters
Engineering visibility shouldn’t require a SaaS subscription. The data is already in your repos. You just need something to assemble it.
When everyone can see the same window, the conversation shifts. Review bottlenecks show up before they become folklore. Someone can point at a heavy week instead of defending a feeling in standup. The goal isn’t a scoreboard. It’s fewer arguments that start and end with vibes.
If you’d run this on your team, let me know. I’m working on an open-source version and documenting the process as I go.
Resources
- Momentum (open-source release in progress)
- Vercel AI SDK
mcp-handler- Model Context Protocol spec