Course ES
← back to chapter

MCP prompts

Paste a code snippet and get a review — but the reviewing instructions don't

Showcase — MCP prompts

Paste a code snippet and get a review — but the reviewing instructions don't live in this app. They're a prompt on an MCP server. The backend calls prompts/get("code_review") with your code, the server renders its own template into ready-to-send messages, and those go to the model. Prompts are the third MCP primitive: tools do actions, resources carry context, prompts hold instructions — so a tool provider can ship the optimized prompt right alongside the tool, versioned together. Improve the server's prompt and every client improves without redeploying.

Run

bash bootstrap-secrets.sh              # reads ../../../../.env, writes secrets/
docker compose up --build              # default: PROVIDER=openai

Open http://localhost:3000.

To run against Gemini instead:

PROVIDER=gemini docker compose up --build

What's where

  • backend/mcp_server.py — the MCP server: code_review and explain_change prompt templates, exposed via prompts/list and prompts/get. It owns the wording; the client only fills the slots.
  • backend/mcp_client.py — the from-scratch MCP client (shared): handshake, then prompts/list / prompts/get.
  • backend/ai_openai.py / backend/ai_gemini.py — fetch the rendered prompt and send its messages to the model.
  • backend/main.py — identical FastAPI loader; reads PROVIDER and dispatches.
  • frontend/app/page.tsx — code box + review.

Stop

docker compose down

Run locally

Download the project as a ZIP and run it with Docker. Brings up a FastAPI backend + Next.js frontend on localhost:3000.

Download mcp-prompts.zip

unzip mcp-prompts.zip
cd mcp-prompts
bash bootstrap-secrets.sh   # one-time: pulls API keys into ./secrets
docker compose up --build   # default provider: openai
# or:  PROVIDER=gemini docker compose up --build

Type some input, pick a provider, and run the same code shown in Source against the live API. Sign-in required.


  

The same modules the Run button hits. The whole project (frontend, Dockerfile, compose) is in the ZIP under README.

backend/ai_openai.py

"""Showcase 3 (OpenAI): prompts the server owns.

The client doesn't write the reviewing instructions — the MCP server does.
We `prompts/get` the `code_review` prompt with the user's snippet, get back
fully-composed messages, and send them straight to the model. Update the
prompt on the server and every client improves without redeploying.
"""
from openai import OpenAI

from mcp_client import MCPClient

_client = OpenAI()

_MODEL = "gpt-5.4-nano"


def run(code: str) -> str:
    with MCPClient() as mcp:
        mcp.list_prompts()  # discovery — a client could let the user pick one
        got = mcp.get_prompt("code_review", {"code": code.strip()})  # prompts/get
        # MCP prompt messages → OpenAI input turns.
        input_list = [
            {"role": m["role"], "content": m["content"]["text"]}
            for m in got["messages"]
        ]
        response = _client.responses.create(model=_MODEL, input=input_list)
        return response.output_text

backend/ai_gemini.py

"""Showcase 3 (Gemini): the same server-owned prompt, a different model.

Same `prompts/get` call, same server-authored messages. Only the mapping to
the model's message format changes (MCP's `assistant` role becomes Gemini's
`model`). The prompt engineering stays on the server, versioned with the tool
it belongs to.
"""
import os

from google import genai
from google.genai import types

from mcp_client import MCPClient

_client = genai.Client(api_key=os.environ["GEMINI_API_KEY"])

_MODEL = "gemini-3.1-flash-lite"

_ROLE = {"user": "user", "assistant": "model"}  # MCP role → Gemini role


def run(code: str) -> str:
    with MCPClient() as mcp:
        mcp.list_prompts()  # discovery — a client could let the user pick one
        got = mcp.get_prompt("code_review", {"code": code.strip()})  # prompts/get
        contents = [
            types.Content(
                role=_ROLE.get(m["role"], "user"),
                parts=[types.Part(text=m["content"]["text"])],
            )
            for m in got["messages"]
        ]
        response = _client.models.generate_content(model=_MODEL, contents=contents)
        return response.text or ""

Project files

  • .gitignore
  • README.es.md
  • README.md
  • backend/Dockerfile
  • backend/ai_gemini.py
  • backend/ai_openai.py
  • backend/main.py
  • backend/mcp_client.py
  • backend/mcp_server.py
  • backend/requirements.txt
  • bootstrap-secrets.sh
  • docker-compose.yml
  • frontend/Dockerfile
  • frontend/app/layout.tsx
  • frontend/app/page.tsx
  • frontend/next.config.ts
  • frontend/package.json
  • frontend/tsconfig.json