Growth Engine: The System I Built to Reach MVP

Growth Engine: The System I Built to Reach MVP

A local app, a Model Context Protocol (MCP) surface and a cloud publishing service. Every night it drafts three LinkedIn posts; I pick one by email, and it goes out with its sources and a check that it is really live.

Project details

Named case study
Industry

Personal platform, dogfooded daily

Timeline

2026, in daily use

Tech Stack

React, TypeScript, Electron, Python, FastAPI, SQLite, Ollama, MCP, Google Cloud Run

One Last Note

One Last Note

Elliot Margot (AI Generated)

The theme for Growth Engine: a machine working through the night, a three-note motif growing into dawn.

0:000:00
Built with
  • React
  • TypeScript
  • Electron
  • Python
  • FastAPI
  • SQLite
  • Ollama (local inference)
  • Model Context Protocol
  • Google Cloud Run
  • Cloud Tasks

Why it exists

Growth Engine started with one goal: become a Microsoft MVP for Copilot Studio within six months. The first version was a gamified tracker, with XP, levels and streaks, for the contribution work that is otherwise scattered across a dozen platforms.

The MVP award came on 1 June 2026. The gamification came out in July, because it had done its job, and the app turned into something more useful: an MVP submission co-pilot (essays, contribution evidence export, readiness scoring and gap analysis) and the engine behind my LinkedIn publishing.

How a post gets published

  1. Night. A scheduled routine drafts three options for the next day, each from a different angle and each with its sources.
  2. Morning. The cloud backend emails me a link to an approval page. I pick one and choose when it goes out.
  3. Publish. The Cloud Run service publishes it through LinkedIn's official API, with its sources attached.
  4. Verify. It reads the post back from LinkedIn and only then tells me it is live.

Nothing goes out that I have not approved, one post at a time.

3 → 1
drafts every night, and exactly one human decision before anything is published

One backend, two ways in

The local app is a React and Electron interface over a FastAPI backend, with model inference running on the laptop. The same backend is exposed as an MCP server with 12 tools: goals, news, communities, profile, and the LinkedIn status, publish, edit and delete actions.

A human opens the interface; an agent calls the MCP surface; both hit one implementation. There is no second copy of the business logic to keep in sync, and no drift between what the interface can do and what an agent can do.

The numbers

  • 52 API endpoints across 18 routers in the local app.
  • About 12,000 lines of Python and TypeScript in the local app, excluding dependencies.
  • 332 automated tests: 101 in the local app (87 backend, 14 front end) and 231 in the cloud service.
  • 12 MCP tools, on the same backend the interface uses.
  • Local inference by default through Ollama, with cloud models only when chosen case by case. A cost and confidentiality choice, not an ideological one.

Responsible AI you can execute

LinkedIn's API terms restrict automated posting. The local app quotes the clause in its configuration file, next to the code it constrains, and its publish button has no scheduler at all. That is deliberate, and the architecture document says so in three words: never add any.

The cloud path exists for the days the laptop is off, and it keeps the human decision: every post it publishes is one I approved, individually, on the approval page.

A charter blocks nothing

Responsible AI usually arrives as a policy document, filed somewhere, referenced in an audit, and structurally incapable of preventing anything.

A constraint placed where someone could actually break it does prevent something. Quoting the terms of service in the config file, beside the function they restrict, means the next person to touch that code reads the reason before they change the behaviour. That is the difference between a principle and a control.

The bug that returned 200

In July the production service drifted: it had been deployed from an uncommitted working tree. The routine kept sending each post's sources, the API kept answering 200, and the data model silently dropped the field it did not know. Posts went out without their sources, and nothing failed loudly.

The fix was a rule rather than a patch: the live service is only ever built from main, from a clean checkout. A green status code proves the request was accepted, not that it did what you meant.

Also in the box

  • A Discord bot that posts the day's Microsoft AI news to the community.
  • A speaking radar that tracks calls for papers, submissions and accepted talks.
  • Contribution evidence export for the MVP renewal.

Both repositories are private, so there is no public link.