Course ES
← back to chapter

MCP resources

A support bot that answers strictly from policy documents — shipping, returns,

Showcase — MCP resources

A support bot that answers strictly from policy documents — shipping, returns, warranty, accounts. The docs are resources on an MCP server: read-only data addressed by doc:// URIs. The backend lists them (resources/list), reads them (resources/read), and puts that text in front of the model as grounding. Where tools are actions, resources are context — the client pulls them in, the model answers, and anything the docs don't cover gets an honest "not covered."

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: a support knowledge base exposing resources/list and resources/read over stdio. No model, just the protocol.
  • backend/mcp_client.py — the from-scratch MCP client (shared with the other showcases): handshake, then resources/list / resources/read.
  • backend/ai_openai.py / backend/ai_gemini.py — pull every resource into context and answer the question grounded in it.
  • backend/main.py — identical FastAPI loader; reads PROVIDER and dispatches.
  • frontend/app/page.tsx — textarea + result.

Try "can I return a final-sale item?" or "does the warranty cover water damage?" — the answers come from the docs, not the model's memory.

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-resources.zip

unzip mcp-resources.zip
cd mcp-resources
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 2 (OpenAI): resources as read-only context.

Resources aren't actions the model takes — they're data the client pulls in.
So the flow is: list the server's resources, read them over MCP, and put that
text in front of the model as grounding. No tool loop; the model answers a
support question using only what the knowledge base says.
"""
from openai import OpenAI

from mcp_client import MCPClient, content_text

_client = OpenAI()

_MODEL = "gpt-5.4-nano"


def _load_context(mcp: MCPClient) -> str:
    """Pull every resource the server offers into one grounding blob."""
    blocks = []
    for r in mcp.list_resources():                      # resources/list
        text = content_text(mcp.read_resource(r["uri"]))  # resources/read
        blocks.append(f"## {r['name']} ({r['uri']})\n{text}")
    return "\n\n".join(blocks)


def run(question: str) -> str:
    with MCPClient() as mcp:
        context = _load_context(mcp)
        response = _client.responses.create(
            model=_MODEL,
            instructions="You are a support agent. Answer using ONLY the policy "
                         "documents below. If they don't cover it, say so — don't "
                         "guess.\n\n" + context,
            input=[{"role": "user", "content": question.strip()}],
        )
        return response.output_text

backend/ai_gemini.py

"""Showcase 2 (Gemini): the same resources, a different model.

Same MCP server, same `resources/list` + `resources/read`. Only the model
call changes. The documents are pulled in as context and the model answers
strictly from them.
"""
import os

from google import genai
from google.genai import types

from mcp_client import MCPClient, content_text

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

_MODEL = "gemini-3.1-flash-lite"


def _load_context(mcp: MCPClient) -> str:
    blocks = []
    for r in mcp.list_resources():                      # resources/list
        text = content_text(mcp.read_resource(r["uri"]))  # resources/read
        blocks.append(f"## {r['name']} ({r['uri']})\n{text}")
    return "\n\n".join(blocks)


def run(question: str) -> str:
    with MCPClient() as mcp:
        context = _load_context(mcp)
        response = _client.models.generate_content(
            model=_MODEL,
            contents=[types.Content(role="user", parts=[types.Part(text=question.strip())])],
            config=types.GenerateContentConfig(
                system_instruction="You are a support agent. Answer using ONLY the "
                                   "policy documents below. If they don't cover it, "
                                   "say so — don't guess.\n\n" + context,
            ),
        )
        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