About the project

AppStory turns a public GitHub repository — or a local folder — into an evidence-backed visual story of an application. Built for the WebMCP Challenge.

Live: appstory.ivanideas.com

Inspiration

Every team I have worked on hits the same wall. A product manager asks a simple question — "What can a user actually do in this app, and where could it break?" — and the only honest answer is "read the code."

Routes, screens, validation, service calls, and tests each hold one piece of the journey. No single document holds all of it. The diagrams that try are stale within a sprint.

Existing tools do not close this gap. Call graphs bury the product in every function. Hand-drawn flow charts read well, but they carry no proof, so nobody trusts them after the code moves on.

The WebMCP Challenge gave me a reason to try a different shape: let an agent read the repository through a small, safe set of browser tools, propose a visual flow, and make a person approve that proposal before it becomes the shared picture. The agent does the reading. The human keeps the authority.

What it does

You connect a public GitHub repository or a local folder. AppStory builds a file index and shows what is approved to read and what is excluded.

Connecting a repo is not the same as letting an agent read it. You grant a separate consent before any source text can leave the page.

A WebMCP agent then searches the approved index, reads bounded source ranges, submits an analysis proposal, and marks it ready for review. You inspect every node — score, factors, cited lines — and every connection's score, then accept or discard the whole proposal.

The accepted flow appears on one canvas and in a keyboard-accessible outline. Expand a screen to see its technical steps. Open the evidence. Review possible gaps.

Every node and connection carries indexed evidence, evidence factors, and an AI confidence estimate. On GitHub, evidence links pin to the analyzed commit. Local folders use a content fingerprint instead of a blob URL. AppStory has no backend, never executes repository code, and never renders source as active HTML.

How I built it

Stack. Vite, React 19, and TypeScript for the single-page app. tldraw for the canvas. use-webmcp-tool to register tools on document.modelContext. oxlint and vitest for the quality gate. Cloudflare Workers for hosting, with a small worker that stamps security headers on every response.

The canvas is a pure projection. This was the key architecture decision. The canvas holds nothing a reader can change. The source of truth is a plain data structure — the accepted analysis — and a single function, proposalToWorld, projects it into tldraw shapes. On every load the app throws away the old canvas and rebuilds it from that data:

// App.tsx — onReady
if (saved.acceptedAnalysis.nodes.length > 0) {
  applyWorld(
    ed,
    proposalToWorld(saved.acceptedAnalysis, saved.projectName, undefined, new Set()),
  );
  window.setTimeout(() => ed.zoomToFit(), 0);
}

Persisting a tldraw snapshot instead would let a stale layout survive a code change. Rebuilding from data every time removed a whole class of bug.

Six WebMCP tools, split by trust.

Tool Purpose
get_project_state Read repository, consent, proposal, and accepted-analysis state
search_repository_index Search approved paths. No source text.
read_repository_source Read one approved range and create a Read Record
get_analysis_state Read a compact index of graph items
submit_analysis_batch Add one validated transactional batch to the draft
finalize_analysis_proposal Mark the draft ready for human review

Read tools use readOnlyHint. Every tool that returns repository or graph text uses untrustedContentHint, because that text was written by a repository author or by the agent, not by me. Finalizing a proposal does not return that text, so it does not carry the hint.

A batch cannot land until its evidence ranges were actually read in this session. That keeps the proposal tied to visible Read Records, not to paths the agent never opened.

Confidence as a formula, not a vibe. Each fact carries evidence references and evidence factors. AppStory reduces them to one raw score.

In words: source-code evidence is worth 30, a test is worth 10, documentation is worth 5, a screen capture is worth 10. Factors then add or subtract — a strong screen implementation is a big plus, a strong conflict is a big minus. The sum is rounded, then clipped to 0–100.

$$ \text{score} = \operatorname{clip}{[0,100]}\left( \operatorname{round}\left( \sum{j \in \text{evidence}} e(\text{src}j) + \sum{i \in \text{factors}} w(k_i)\,m(s_i) \right) \right) $$

where $e(\cdot)$ weights an evidence reference by its source (source code $30$, test $10$, docs $5$, screen capture $10$), $w(k_i)$ weights the factor kind, and $m(s_i)$ scales by factor strength.

Two rules then override the number. A fact with no traceable evidence is forced to $0$. A score that would look Confirmed is capped at $79$ if there is no direct source line, or if there is a strong conflict:

$$ \text{score} \leftarrow 79 \quad \text{if } \text{score} \ge 80 \ \wedge \ \big(\lnot\text{direct source evidence} \ \vee \ \text{strong conflict}\big) $$

$$ \text{label} = \begin{cases} \textbf{Confirmed} & \text{score} \ge 80 \ \textbf{Inferred} & 40 \le \text{score} < 80 \ \textbf{Unknown} & \text{score} < 40 \ \text{or the fact is not traceable} \end{cases} $$

Confidence describes evidence quality, never business impact. Those two ideas stay on separate controls.

Defense in depth for secrets. In local-folder mode a weak filter leaks a developer's real credentials, so exclusion runs in layers:

  • Secret and dependency patterns are checked on every path segment, not just the filename. Segments are NFC-normalized and stripped of zero-width characters first.
  • A content scan backs up the name filter. A file whose text matches a private-key block, a known token prefix, or a quoted credential assignment is blocked even when the name looked safe.
  • Imported Project Files are re-validated for path traversal. The Markdown export degrades a bad evidence link to plain text instead of throwing.

Every response carries Origin-Agent-Cluster, Permissions-Policy: tools=(self), X-Content-Type-Options, Referrer-Policy, and X-Frame-Options — the same five headers in all four host configs (Cloudflare Workers, public/_headers, Netlify, and Vercel).

Challenges I ran into

Treating the agent's output as hostile. The natural instinct is to trust a tool result. Here I could not. A repository path, a source excerpt, and the agent's own proposed titles are all free text from outside my trust boundary. Every batch is validated as one transaction: an invalid evidence reference, node kind, endpoint, identity, or duplicate rejects the whole batch and changes nothing. Getting that all-or-nothing behavior right, with clear messages, took more test cases than the feature code.

Keeping the proposal separate from the accepted graph. submit_analysis_batch and finalize_analysis_proposal never touch what is on the canvas. Finalization only marks a draft ready. Only a human click accepts it. Modeling two parallel analysis states — draft and accepted — and diffing them for the review screen (added, changed, possibly removed) was the hardest state-management work in the project.

The progressive graph. A fully expanded graph hides the product journey in technical noise. Separate per-flow graphs hide the screens that flows share. The answer was one graph that starts as a flow overview and expands a screen into its technical steps in place, on request. Layout had to stay stable as nodes appeared and disappeared.

WebMCP is still arriving. No shipping browser exposes document.modelContext without a flag or an origin-trial token. Unit tests cover indexing, consent gates, transactional batches, and the canvas projection. Native WebMCP still needs a person to flip a flag or register an origin trial. The status chip polls for fifteen seconds after mount, because a browser extension can inject the API after the page loads, and a one-shot check would leave the chip wrongly stuck on unavailable.

Accessibility parity. The canvas cannot be the only way in. The keyboard outline renders from the same accepted analysis data: evidence factors, AI reasons, connections, read records, and the gap-review controls. On GitHub, those evidence links are pinned to the analyzed commit.

What I learned

Make the visual layer disposable. When the canvas is a pure projection of plain data, a redesign is a new projection function, and reload bugs disappear.

Trust boundaries belong in the type system. Naming a value untrusted — in a hint, a type, a validator — changes how you write every function that touches it.

A human-in-the-loop step is a feature, not a delay. The review screen is where a mixed team actually builds a shared understanding. Removing it would remove the point of the product.

Confidence needs a definition. Once the score was a written formula with a conflict penalty, disagreements moved from "does this feel right" to "is this factor weighted correctly" — a much better argument to have.

What's next

  • Register the origin trial so every visitor gets native WebMCP with no flag.
  • Measure tldraw's inline styles so a real Content-Security-Policy can ship.
  • More node kinds for background jobs and scheduled work.
  • A compare view across two commits of the same repository.

Built With

Share this project:

Updates

Submission history