www
MCP serverEverything elseRead-only portfolio tools, resources, and prompts for Fmind.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use www
From the project's README
As published by fmind/www in README.md.
The portfolio website of Médéric Hurier (Fmind). It is a fully server-rendered Python application built with Litestar, Jinja, Tailwind CSS v4, and DaisyUI v5. Small vanilla JavaScript menu and calculator controllers provide progressive enhancement. There is no client framework, Node.js application project, database, cookie, or analytics tracker.
Highlights
- Server-rendered by Litestar and strict, autoescaped Jinja templates packaged under
src/www/templates/; Granian serves the ASGI application. - Fast: inlined critical CSS, Brotli/gzip page compression, content-hashed immutable static caching, self-hosted subset fonts, and
content-visibilityfor below-the-fold sections. Static files keep their on-disk bytes so strong ETags, byte ranges, and HEAD responses agree. - Hardened: a strict, per-request-nonce CSP with no
unsafe-inlineorunsafe-evalscripts, the full security-header suite, MCP cross-origin protection, and a request-body cap on the public/mcpendpoint. - Article-native: strictly validated Markdown, responsive self-hosted media, server-side Pygments highlighting, full-text search and tag filtering at
/articles/, social and JSON-LD metadata, related articles, Atom, raw Markdown at/articles/<slug>.md, and a canonical-only sitemap. - Sites: source-backed calculators under
/sites/use shareable GET assumptions, server-owned formulas, explicit cost boundaries, and progressive enhancement. The LLM hosting tool compares demand, batch/cache API pricing, capacity, measured latency, and cost per accepted task. - Agent-ready: a closed-world, read-only Model Context Protocol server at
/mcpwith tools, resources, prompts, server-card discovery,/api/profile,/llms.txt,/llms-full.txt, and connected JSON-LD graphs. - Privacy-first: no cookies, visitor identifiers, third-party scripts, runtime CDN dependencies, or client-side analytics. One aggregate record per HTML response can be routed from Cloud Logging to a 180-day partitioned BigQuery dataset without retaining an IP address, user-agent string, full referrer, session, or trace identifier.
- Observable: optional OpenTelemetry export through
OTEL_EXPORTER_OTLP_ENDPOINT, withtrace_idandspan_idcorrelation in structured logs.
Prerequisites
- Python 3.14 (mise and the production image pin 3.14.7)
- mise for the pinned Python,
uv, browser, infrastructure, security, and formatting toolchain - Docker Engine with the Buildx plugin for the production image gate in
mise run all
A fresh checkout needs network access for mise install. The first mise run all also installs Chromium, pulls the production base image, and refreshes vulnerability databases. Warm application tests remain local and need no cloud credentials.
Local Development
mise install # install the pinned toolchain
mise run install # sync the locked development environment and install Chromium
mise run watch # run Granian with reload alongside the Tailwind watcher
The site is served at http://localhost:8080. Configuration is environment-driven through .env.example; with no environment set, it runs in development mode on port 8080. Use PORT=8081 mise run watch when the default port is occupied.
src/www/app.py is the composition root and exports www.app:app. src/www/__main__.py validates runtime configuration and starts Granian. Jinja templates are package data, while content/ and static/ remain explicit deploy-time trees. The production image copies both beside the locked virtual environment under /app.
Articles
Articles live in content/articles/<slug>.md, with images under static/img/articles/<slug>/. Each source begins with strict TOML frontmatter containing title, description, date, tags, slug, optional updated, external canonical, and syndicated, plus draft. Unknown keys or tags, invalid dates, duplicate slugs, and missing cover assets fail application construction. Production excludes drafts from pages and every discovery surface; development renders them with noindex for review.
Tags use the closed vocabulary in src/www/tags.py: Agent, Coding, LLM, RAG, MLOps, Cloud, Python, Project, Demo, and Guide. Each needs a matching [data-tag='…'] rule in assets/css/input.css and must tag at least one article. Declaration order is display order.
src/www/content.py parses and normalizes Markdown with markdown-it-py, shifts body headings beneath the page title, enhances body images, and folds a repeated image-alt paragraph into a caption. Pygments highlighting is centralized in src/www/highlighting.py. The first body image is preloaded; later figures load lazily. Standalone illustrations break out of the 896px text column up to the 1280px figure width, link to their full-resolution source, and never pan horizontally.
Sources are bounded by a roughly 2.4MP pixel budget. Each image ships only the responsive rungs it can support (<stem>-800.webp and <stem>-1280.webp). Generate and commit them with mise run build:images; the provenance lock binds exact source and target SHA-256 digests to the pinned Pillow/WebP encoder recipe, then rebuilds only changed, missing, tampered, or recipe-stale rungs. The canonical check runs check:images, which validates the complete archive without writing.
The validated article collection is the sole source for HTML, search, raw Markdown, Atom, sitemap, LLM text, JSON, and MCP publication surfaces. Markdown responses make rendered root-relative links absolute while preserving code examples and external URLs. Article changes follow the authorized article publication workflow; this repository owns the published body.
Branding
The light website palette follows fmind/theme: white canvas, light gray panels, charcoal text, and the logo blue #174EA6 for links, controls, and focus. assets/css/input.css owns interface colors; src/www/highlighting.py maps the same syntax roles to the existing token classes (blue keywords/functions, green strings, orange literals, purple types, gray comments, and red errors). Update both from the theme’s checks/palette.yaml and ptpython/fmind.py; the website builds independently of that checkout.
The full-resolution, losslessly optimized PNG masters are available at https://www.fmind.dev/logo.png and https://www.fmind.dev/banner.png. These stable URLs serve PNG bytes directly with cache revalidation and also retain the artwork's transparency for reuse.
Navigation uses a 96px lossless WebP logo for the 48px display, while favicons and home-screen icons use appropriately sized derivatives. Default social previews fit the complete banner onto a white 1200×630 progressive JPEG canvas; article previews retain their own covers. Regenerate these committed assets from static/logo.png and static/banner.png with mise run build:branding. Home-screen icons declare any because the logo's outer ring extends beyond the maskable safe area.
Conference Contact Page
Share https://www.fmind.dev/connect at conferences. The page puts LinkedIn first, followed by a downloadable contact card, email, and the portfolio. /connect.vcf derives its public name, role, email, website, and LinkedIn from the portfolio data at application startup. Visitors confirm saving the contact in their own app; some Android browsers require importing the downloaded .vcf from Contacts.
The page displays a QR code encoding the permanent /connect URL, so contact actions can change without replacing the code. The page and contact download work without JavaScript or third-party QR services.
Regenerate the displayed QR asset with the pinned Segno CLI; it is a build-time tool, with no application dependency:
uvx --from segno==1.6.6 segno --error M --border 4 --scale 8 --light white --output static/img/connect-qr.svg https://www.fmind.dev/connect
Decision Tools
Focused tools live under /sites/. src/www/data.py:SITE_PAGES drives metadata, the sitemap, LLM text, JSON, MCP discovery, and article relationships; src/www/app.py wires each page's route, template, and view builder explicitly. Calculation models and validation live under src/www/sites/; Jinja owns presentation; native GET forms keep scenarios linkable. Follow the repository's site skill when adding or revising one.
The first tool, /sites/llm-self-hosting/, compares the current top ten open-weight models from Artificial Analysis with editable GKE accelerator, memory, utilization, staffing, and managed-API assumptions. It includes L4 and RTX PRO 6000 nodes, separate on-demand and resource-CUD billing choices, and a compact hardware starting point based on memory fit and whole-machine cost. Commitments remain billed while idle. It is a planning calculator: ranks and list prices carry a visible snapshot date, and throughput remains a workload-specific input that must come from a pilot. The source snapshot records verified rates, scope, and recommendation rules.
Tasks
All tasks are defined in mise.toml and reused by Lefthook and CI:
| Task | Description |
|---|---|
mise run install | Frozen uv sync, Lefthook installation, and pinned Chromium installation |
mise run watch | Granian ASGI reload server and Tailwind watcher |
mise run format | Ruff Python imports/formatting, dprint, and OpenTofu formatting |
mise run check | Ruff, ty, metadata/lock, dependency, Dockerfile/IaC, and workflow checks |
mise run check:image | Build and scan the exact production OCI archive (Docker with Buildx required) |
mise run check:image:deployed | Resolve and scan every serving Cloud Run revision with Trivy |
mise run check:images | Validate all image derivatives, hashes, modes, dimensions, and recipe without writing |
mise run check:typos | Check article prose against the spelling floor |
mise run check:links | Check external content links (network-dependent; scheduled weekly in CI) |
mise run check:tofu | Validate and lint OpenTofu (network-dependent; runs in CI on infra/ changes) |
mise run test | Run pytest offline with branch coverage of at least 85% |
mise run test:browser | Run Chromium journeys on desktop and mobile in light mode |
mise run test:image | Smoke-test the already-built production OCI archive through Docker |
mise run test:lighthouse -- --base-url <origin> [--mode full|smoke|portfolio] | Run the strict five-category Lighthouse matrix; never in all |
mise run coverage | Show the terminal coverage report |
mise run build | Compile Tailwind CSS and build clean wheel and source distributions |
mise run build:images | Reconcile changed, missing, tampered, or recipe-stale Pillow derivatives |
mise run build:image | Build the production OCI image archive at tmp/www-image.tar |
mise run deploy <digest-ref> | Roll Cloud Run to one repository-pinned image digest (manual and production-mutating) |
mise run all runs sequentially: format, static checks, the package/CSS build, pytest, the exact OCI archive build and scan, its bounded runtime smoke test, one pinned Chromium installation, then browser journeys. The image smoke reuses the archive produced by check:image; it never builds. Docker Engine with Buildx must be running, and a cold run needs network access as described above.
Lighthouse remains an explicit, non-default audit because it needs a target origin. Full mode audits every sitemap page on desktop and mobile, then adds 48 stress audits; its count grows with the sitemap. Install Chromium first with mise run install:browser; if it is absent, the harness reports the equivalent playwright install chromium remediation. Use mise run test:lighthouse -- --base-url http://127.0.0.1:8080 --mode smoke for the four-audit local smoke matrix, use --mode portfolio for eight audits covering /, /connect, /articles/, and /sites/ without article or decision-page bodies, or omit --mode for full qualification. --plan validates tools without fetching the sitemap, so full-mode totals are unknown until execution. To exercise the browser suite against production, use BROWSER_BASE_URL=https://www.fmind.dev mise run test:browser. Reports and traces stay under tmp/. Local application checks need no cloud credentials.
Configuration and working-tree secret scans exclude generated tmp/, .venv/, and dist/ trees. Secret scans of Git history keep the default rules.
Operational commands live in scripts/deploy.py and scripts/image_smoke.py, outside the shipped application package. The image smoke uses the MCP SDK with bounded loopback HTTP requests and checks image identity, non-root execution, file permissions, HTTP, tools, resources, and prompts. mise keeps their public task names stable. Browser tooling keeps its Node dependencies in mise installations; there is no repository Node application or runtime Plotly dependency.
Deployment
The site runs on Google Cloud Run in project www-fmind-dev, region europe-west1, and is served at https://www.fmind.dev/. Existing GCP identifiers retain www-fmind-dev to avoid an unrelated production migration. OpenTofu under infra/ owns the Cloud Run shape, Artifact Registry, keyless GitHub Actions identity, alerts, and privacy-preserving analytics route.
- Continuous delivery — pull requests and pushes run
.github/workflows/deploy.yml. Actions use immutable commit pins, updated through Dependabot. Amainpush builds the non-root OCI image with provenance and an SBOM, pushes it to Artifact Registry, scans and smoke-tests that exact digest, then deploys it through Workload Identity Federation restricted tomainand the immutable GitHub repository and owner IDs. - Infrastructure —
tofu -chdir=infra init && tofu -chdir=infra applyis always manual. OpenTofu owns CPU, memory, scaling, environment, probes, and IAM; CI owns only the image digest throughlifecycle.ignore_changes. - Runtime — Cloud Run injects
PORT, OpenTofu setsENVIRONMENT=production, and standardOTEL_EXPORTER_OTLP_*variables enable tracing. The container runs as UID/GID 10001 with a locked virtual environment,content/, andstatic/under/app. - Local image — run
mise run build:image, load the archive withdocker load --input tmp/www-image.tar, then rundocker run -p 8080:8080 www:local. - Manual rollout or rollback —
mise run deploy europe-west1-docker.pkg.dev/www-fmind-dev/app/www-fmind-dev@sha256:<64-lowercase-hex>accepts exactly one digest from the production repository and changes only the service image. Tags, foreign repositories, and extra flags fail beforegcloud; the task never runs from a hook ormise run all. Before a new rollout, qualify that exact digest withmise run check:image:digest <digest-ref>andmise run test:image <digest-ref>; rollback should select a previously qualified digest.
Reconcile reviewed infra/ changes before rolling out an application image. The Python runtime is qualified for the declared concurrency of 8, a 512 MiB service limit, scale-to-zero with at most 5 instances, and the startup probe's 6 attempts at 5-second intervals. CI and the manual deploy task change only the image; merging a branch does not apply service settings. After each image rollout, verify health, traffic, error logs, and browser journeys; keep the previous known-good digest available for rollback.
The weekly security workflow scans full Git history and source, then uses a separate read-only cloud identity to resolve and scan every image receiving traffic. It retains fixed and unfixed HIGH/CRITICAL advisory reports for 30 days and enforces the same fixable finding gate as deployment. SECURITY.md records the review owner, residual exposure, and next review. GitHub secret scanning, push protection, dependency alerts, and Dependabot security updates are enabled; main rejects deletion and force pushes while allowing ordinary owner pushes.
Analytics
Each HTML response emits one aggregate structured record: path, status, referrer host, UTM dimensions, and a bot flag. The schema keeps country empty because the public Cloud Run origin has no trusted geography header. A Cloud Logging sink routes records to a BigQuery dataset partitioned daily with a 180-day expiry. These aggregate analytics records retain no cookie, IP address, user-agent string, full referrer, session, trace identifier, or derived location. Cloud Run separately writes private operational request logs, which can contain IP addresses, user agents, and complete request URLs; the Cloud Logging _Default bucket retains those logs for 30 days. Query recipes live in the infra skill.
Connecting an AI Agent to /mcp
After deployment, add https://www.fmind.dev/mcp as a custom MCP connector. The server exposes the closed-world, read-only tools get_profile, list_experience, list_certifications, list_publications, search_articles, list_projects, and get_services; the portfolio://profile.json resource; and the assess_fit and brief_me prompts. The JSON profile advertises /api/profile/schema.json through a Link: rel="describedby" header; its JSON Schema derives from the same public serialization model. Server-card metadata, including the resource URI and MIME type, is available at /mcp/server-card and the well-known compatibility route. Browser calls are accepted only from the same origin; non-browser clients need no authentication.
The homepage, JSON profile, JSON-LD occupation skills, and LLM text derive expertise from the same EXPERTISE collection. Both headline lines and the six expertise descriptions are included in /llms.txt and /llms-full.txt. These are integration surfaces; they do not guarantee search indexing or AI citations.
Use the standard server/discover RPC for protocol negotiation and capabilities. The static server card is a compatibility summary of the registered primitives, not an official MCP schema or an A2A Agent Card.
The checked-in server.json describes io.github.fmind/portfolio in the official MCP Registry. Its version tracks the website release. After verifying that version is serving, publish with the official mcp-publisher CLI authenticated as the fmind GitHub account:
mcp-publisher validate server.json
mcp-publisher login github
mcp-publisher publish
Verify the published version through the official Registry API. Registry publication is an explicit owner action separate from website deployment.
Advanced
- Delivery
- portfolio MCP server → your ahel gateway (mcp.ahel.ai) → every connected AI client.
- Catalog kind
- mcp-server
- Gateway key
io-github-fmind-portfolio- Source
- github.com/fmind/www
- Hosted endpoint
https://www.fmind.dev/mcp