Skip to main content
Glama
gonzalogarcia-ai

ailab_rip_mcp_app

README.md
# ailab_rip_mcp_app

A foundational demo of a Model Context Protocol (MCP) app. A tiny FastMCP
server exposes a stock-quote tool from a bundled JSON file, runs locally over
streamable HTTP, deploys to AWS via ECS Express Mode, and plugs into ChatGPT
as a Developer Mode custom connector.

The point is the walkthrough, not the depth. The code is short enough to read
in one sitting.

## What is MCP

The [Model Context Protocol](https://modelcontextprotocol.io) is an open
standard for wiring LLMs to external tools and data. An **MCP server** exposes
capabilities — most commonly **tools** (functions the model can call) — and
an **MCP client** (ChatGPT, Claude, your own agent) discovers and invokes them
over a transport such as stdio or streamable HTTP. This repo is an MCP server.

## Prerequisites

- Python 3.10+
- [`uv`](https://astral.sh/uv) for dependency management (`brew install uv`
  or `curl -LsSf https://astral.sh/uv/install.sh | sh`)
- Docker (for the AWS deploy)
- AWS CLI v2 with a configured profile that can create ECR repos and ECS
  services
- Node.js (only for `npx @modelcontextprotocol/inspector` during local
  verification)
- A ChatGPT account that can enable Developer Mode — see the
  [ChatGPT integration](#integrate-with-chatgpt-developer-mode) section

## Repo tour

```
.
├── src/
│   ├── server.py          # FastMCP server, two tools + widget resource
│   └── data/quotes.json   # 10 hand-curated tickers
├── web/                   # React + Vite widget (Apps SDK compatible)
│   ├── src/               #   StockCard.tsx, main.tsx, styles.css
│   ├── dist/stock-card.js #   built bundle (committed — server needs no npm)
│   └── package.json
├── tests/                 # pytest + in-memory FastMCP Client
├── scripts/push_to_ecr.sh # build + push helper
├── Dockerfile             # uv-based image, exposes 8000
├── pyproject.toml         # uv-managed deps
└── uv.lock                # locked deps (committed)
```

## Install and test

```bash
uv sync                # create .venv from uv.lock
uv run pytest          # 4 tests, in-memory, ~40 ms
```

## Run locally and verify with MCP Inspector

```bash
uv run python src/server.py
# → FastMCP banner shows http://0.0.0.0:8000/mcp
```

In another shell:

```bash
npx @modelcontextprotocol/inspector
```

In the Inspector UI: Transport = **Streamable HTTP**, URL =
`http://localhost:8000/mcp`, then invoke:

- `list_supported_symbols` → 10 tickers
- `get_stock_quote` with `symbol="AAPL"` → the seeded dict + `day_change_pct`
- `get_stock_quote` with `symbol="ZZZZ"` → a tool error naming the supported
  symbols

## Deploy to AWS with ECS Express Mode

[ECS Express Mode](https://aws.amazon.com/blogs/aws/build-production-ready-applications-without-infrastructure-complexity-using-amazon-ecs-express-mode/)
is AWS's newest single-command path for containerized services. It
auto-provisions the ECS cluster, Fargate task, ALB with HTTPS, target group,
security groups, and auto-scaling from one API call — exactly what a ChatGPT
connector needs.

### 1. Build and push the image to ECR

```bash
./scripts/push_to_ecr.sh
# → prints IMAGE_URI=<account>.dkr.ecr.<region>.amazonaws.com/mcp-stock-quote:latest
```

Overridable env vars: `AWS_REGION`, `AWS_PROFILE`, `ECR_REPO_NAME`,
`IMAGE_TAG`. The script builds for `linux/amd64` explicitly — required for
Fargate x86 tasks when the dev machine is Apple silicon.

### 2. Deploy to ECS Express (via `glab_iac`)

Infrastructure lives in the sibling [`glab_iac`](../glab_iac) Terraform
repo. Add a new ECS Express block that points to the ECR image you just
pushed, then apply:

1. In `glab_iac`, add a new ECS Express service block. Set:
   - `image` → the `IMAGE_URI` printed by `push_to_ecr.sh`.
   - `container_port` → `8000` (what the FastMCP server binds to).
   - `health_check_path` → `/health`.
2. `terraform plan` and review — Express Mode auto-provisions the cluster,
   Fargate task, ALB with HTTPS, target group, security groups, IAM roles,
   and auto-scaling.
3. `terraform apply`. Wait until the service reports `RUNNING` and its ALB
   target group reports **healthy** (a few minutes).
4. Grab the public HTTPS URL from the Terraform output (or the ECS
   console). Your MCP endpoint is `https://<generated-domain>/mcp`.

Verify with MCP Inspector or a quick `curl` before wiring ChatGPT:

```bash
curl -s https://<generated-domain>/health   # → {"status":"ok"}
```

Then integrate it as a custom connector — see the next section.

> **Region caveat:** ECS Express Mode is new (announced late 2025) and not
> yet available in every AWS region. Check the ECS console in your region.

## Integrate with ChatGPT (Developer Mode)

All quotes below come from
[OpenAI's custom MCP docs](https://developers.openai.com/api/docs/mcp).

### Requirements

- **ChatGPT subscription** — Developer Mode is a gated feature. OpenAI's
  developer docs don't spell out the tier list; the authoritative source is
  [help.openai.com article 11487775](https://help.openai.com/en/articles/11487775).
  Historically: Plus, Pro, Business, Enterprise, Edu. Free tier cannot follow
  this section.
- **Developer Mode toggled on** —
  > "In ChatGPT, open **Settings → Security and login** and turn on
  > **Developer mode**."
- **Server transport** — public HTTPS URL, streamable-HTTP transport, path
  `/mcp`. ECS Express Mode + FastMCP's defaults give you all three.


**For this demo we use no authentication.** OpenAI's docs neither mandate nor
explicitly forbid it — a personal Developer Mode server only you connect to
works fine unauthenticated, and it keeps the demo focused on MCP itself.
**This is dev-only.** Production connectors should use OAuth.


### Add the connector

1. ChatGPT → **Menu → Plugins → Add (+)**.
2. Paste `https://<ecs-express-domain>/mcp`.
3. Leave authentication empty: No Auth (dev-only).
4. Save.
5. Connect.
6. Check at Plugins (browse until you find yours).

## Try it

Prompt ChatGPT:

> Using the stock quote connector, what is AAPL's current price?
> @stock_mcp_app what is AAPL's current price?

The trace panel should show a call to `list_supported_symbols` and/or
`get_stock_quote`. Both tools are read-only, so no confirmation prompt
appears.

## The widget (sharing UI back to ChatGPT)

Chapter one served pure JSON and let ChatGPT paint a paragraph. Chapter two
attaches a small React "stock card" to every `get_stock_quote` result so
ChatGPT renders it inline. Two pieces make this work:

1. **The tool declares its template and a typed return.** In `src/server.py`:

   ```python
   @mcp.tool(meta={"openai/outputTemplate": "ui://stock-card/v1.html"})
   def get_stock_quote(symbol: str) -> Quote:
       ...
   ```

   Two things matter here: the `_meta` field points at the widget resource,
   and the `Quote` TypedDict return makes FastMCP emit an `outputSchema`.
   Without that schema, ChatGPT filters `window.openai.toolOutput` down to a
   handful of fields — the widget wouldn't see the price.

2. **The resource ships the widget.** Same file:

   ```python
   @mcp.resource("ui://stock-card/v1.html", mime_type="text/html;profile=mcp-app")
   def stock_card_widget() -> str:
       return _WIDGET_HTML  # <div id="root"></div> + inlined React bundle
   ```

   The mime type `text/html;profile=mcp-app` is what tells ChatGPT to render
   this inside a sandboxed iframe next to the assistant reply. The React
   bundle in `web/dist/stock-card.js` reads `window.openai.toolOutput` (found
   by walking up the frame chain to the host frame) and paints a price card.

**Editing the widget.** Only needed if you want to change the UI itself — the
server ships the built bundle:

```bash
cd web
npm install
npm run build   # -> web/dist/stock-card.js
```

The bundle is committed on purpose so `uv run python src/server.py` works
without a JS toolchain. Rebuild the container after any widget change so the
new bundle ships to ECS. In ChatGPT, hit **Refresh** on the connector (or
remove + re-add) so it picks up any schema changes, then start a new chat.

**Gotchas we hit.** Two Apps SDK details worth knowing:

- Vite's `lib` mode doesn't replace `process.env.NODE_ENV`. Without
  `define: { "process.env.NODE_ENV": '"production"' }` in `vite.config.ts`,
  the widget throws `ReferenceError: process is not defined` in the sandbox.
- `window.openai` is only injected on the outer host frame. Widgets rendered
  in a nested iframe must walk up (`window.parent`, `window.top`) to reach
  it — see `web/src/main.tsx`.

## Limitations and next steps

- **Static data** — real prices need something like
  [`yfinance`](https://pypi.org/project/yfinance/); note its ToS.
- **No auth** — add OAuth via FastMCP's built-in
  [auth providers](https://gofastmcp.com/servers/auth) before making the
  server public.
- **No infrastructure-as-code** — Express Mode is one CLI call. For repeatable
  environments, port to CloudFormation
  ([aws-samples/sample-mcp-server-on-ecs](https://github.com/aws-samples/sample-mcp-server-on-ecs))
  or CDK.
- **Single-stage Docker image** — split builder/runtime stages to shrink the
  image.
## Links

- FastMCP docs — <https://gofastmcp.com>
- OpenAI custom MCP connectors — <https://developers.openai.com/api/docs/mcp>
- ECS Express Mode launch post — <https://aws.amazon.com/blogs/aws/build-production-ready-applications-without-infrastructure-complexity-using-amazon-ecs-express-mode/>
- Deploying MCP on ECS (AWS blog) — <https://aws.amazon.com/blogs/containers/deploying-model-context-protocol-mcp-servers-on-amazon-ecs/>
- uv — <https://astral.sh/uv>

Maintenance

ActivitySlowing
ResponsivenessNo issues