Skip to main content
Glama
README.md
# biz-mcp

A custom MCP (Model Context Protocol) server with **7 tools** that connect to an LLM/MCP client. It combines:

- **2 external API wrappers** - live FX rates (ECB/Frankfurter) and crypto prices (CoinGecko), both free and key-less.
- **2 database operations** - a key-value store backed by DynamoDB on AWS (falls back to a local JSON file for development).
- **3 business-logic tools** - invoice calculation, shipping estimation, and inventory-status assessment.

It runs on **AWS Lambda** behind a **Lambda Function URL** using the modern **streamable HTTP** MCP transport. The same code runs locally over **stdio** (for Claude Desktop) or **streamable HTTP**.

## Tools

| Name | Category | Description |
| --- | --- | --- |
| `get_exchange_rate` | External API | ECB reference-rate FX conversion (frankfurter.app, no key) |
| `get_crypto_price` | External API | Current crypto price (CoinGecko public API, no key) |
| `save_record` | Database | Put an arbitrary JSON object under `(pk, sk)` |
| `get_record` | Database | Read a JSON object under `(pk, sk)` |
| `calculate_invoice_total` | Business logic | Subtotal / discount / tax / total from line items |
| `estimate_shipping` | Business logic | Shipping cost from weight, distance and zone |
| `check_inventory_status` | Business logic | Reads a record and classifies stock as in_stock / low_stock / out_of_stock |

## Architecture

```
MCP client (Claude Desktop, Cursor, mcp inspector)
        │  streamable HTTP (POST JSON-RPC / SSE)
        ▼
Lambda Function URL  ──►  lambda_function.py (protocol adapter)
                                   │
                                   ▼
                            mcp_server.py (FastMCP, 7 tools)
                              ├── apis.py      → frankfurter.app, CoinGecko
                              ├── db.py        → DynamoDB (or local JSON)
                              └── business.py  → invoice, shipping, inventory
```

## Project layout

```
apis.py             External API wrappers
business.py         Pure business logic
db.py               Key-value store (DynamoDB or local JSON)
mcp_server.py       FastMCP server defining the 7 tools
lambda_function.py  Streamable-HTTP JSON-RPC adapter for Lambda
template.yaml       SAM template (Lambda + Function URL + DynamoDB)
deploy.ps1          One-command deployment for Windows
tests/              pytest suite (unit + lambda handler + stdio E2E)
```

## Local setup

```powershell
python -m venv .venv
.\.venv\Scripts\Activate.ps1
pip install -r requirements-dev.txt
```

### Check the tests

```powershell
pytest
```

### Run locally

- **stdio** (default, for Claude Desktop and the inspector):

```powershell
python mcp_server.py
```

- **streamable HTTP** (for browsers / HTTP clients):

```powershell
python mcp_server.py --transport http
# serves at http://127.0.0.1:8000/mcp
```

### Try it with the MCP Inspector

```powershell
npx @modelcontextprotocol/inspector python mcp_server.py
```

## Deploy to AWS Lambda

Prerequisites: [AWS CLI](https://aws.amazon.com/cli/) (`aws configure`) and [SAM CLI](https://aws.amazon.com/serverless/sam/).

```powershell
.\deploy.ps1
```

The script builds dependencies (`build_deps.ps1`), packages the template, and creates/updates the stack. `sam deploy`/`sam package` creates:

- a Lambda function (`biz-mcp`) with the Python 3.12 runtime,
- a **Lambda Function URL** (HTTPS, `AuthType: AWS_IAM` — every request must be SigV4-signed) with permissive CORS,
- a **DynamoDB** table (`biz-mcp-store`, `pk`/`sk` composite key, on-demand capacity) plus the IAM policy so the Lambda can access it,
- a dedicated **client IAM user** (`biz-mcp-client`) with an access key, allowed to invoke the Function URL (this is what MCP clients use to authenticate),
- Lambda permission entries scoped to exactly that IAM user.

Notes on the deploy path:

- `FunctionUrl.Cors.AllowMethods` does **not** accept `OPTIONS` (supported enum: `GET | PUT | HEAD | POST | PATCH | DELETE | *`). Omit it; Lambda handles preflight automatically.
- Deployment uses `sam package` + `aws cloudformation create-stack`/`update-stack` directly rather than a changeset, because the CloudFormation `AWS::EarlyValidation::PropertyValidation` hook (Nov 2025) can spuriously fail changeset creation. `sam build` is not run on Windows; dependencies are built into `build/mcp.zip` by `build_deps.ps1` via `uv`.
- The stack creates a named IAM managed policy, so pass `CAPABILITY_NAMED_IAM` as well as `CAPABILITY_IAM`/`CAPABILITY_AUTO_EXPAND`.

Manual equivalents:

```powershell
.\build_deps.ps1                                                        # build build/mcp.zip
sam package --template-file template.yaml --resolve-s3 --output-template-file packaged.yaml
aws cloudformation create-stack --stack-name biz-mcp --template-body file://packaged.yaml `
    --capabilities CAPABILITY_IAM CAPABILITY_AUTO_EXPAND CAPABILITY_NAMED_IAM
```

### Point an MCP client at it

The Function URL is `AuthType: AWS_IAM`, so an HTTP MCP client must **sign every
request with SigV4** using the `biz-mcp-client` IAM user's credentials (the access
key is printed in the stack outputs after deploy). Plain HTTP clients without
SigV4 will get `403 Forbidden`.

Generic MCP apps that cannot sign (plain HTTP config in Claude Desktop, etc.)
will be rejected; use the bundled Python agent, or a client that supports SigV4.

## Run the bundled agent (free Groq brain + SigV4 auth)

`agent/agent.py` is a ready-made agent that connects to the MCP endpoint, signs
every request with SigV4 (using the `biz-mcp-client` IAM user), and drives the
7 tools with a **free-tier Groq** model using native function calling.

```powershell
# use the project venv so openai/mcp deps don't clash with other Python installs
& "C:\Users\lenovo\AppData\Local\Programs\Python\Python312\python.exe" -m venv .venv
.\.venv\Scripts\Activate.ps1
python -m pip install -r agent/requirements.txt
# get a free key once at https://console.groq.com/keys
setx GROQ_API_KEY "gsk_..."

# the client IAM credentials - either env vars, or write them to
# agent\.env (gitignored) as:
#   AWS_ACCESS_KEY_ID=AKIA...
#   AWS_SECRET_ACCESS_KEY=wJal...
# (both work; agent\.env wins when the env vars are unset)

python agent/agent.py --url https://YOUR_ENDPOINT/
# or a one-shot question:
python agent/agent.py --url https://YOUR_ENDPOINT/ `
  --question "Convert 250 USD to EUR, then save it as pk=1201 sk=usd-eur"
```

The agent lists the MCP tools, then chains tool calls until it can answer (it can
save/read records in DynamoDB, check inventory, price orders and shipping, and pull
live FX/crypto rates). Check the current free model id with `GET /openai/v1/models`
and pass it via `--model` if the default is unavailable.

## Smoke test without a client

The endpoint is IAM-authenticated, so a bare `curl` returns `403 Forbidden`. To
call it you must SigV4-sign each request. Two easy options:

- Use the bundled agent (it signs automatically).
- Use the AWS CLI as a signed HTTP client:

```powershell
# AWS CLI supports SigV4-signed HTTP calls via --request-url / --header-ish flags
aws lambda invoke --function-name biz-mcp --region us-east-1 out.json
```

`tools/list` will return the 7 tools (use the agent with `--question "What tools do you have?"`).

## How the transport works

`lambda_function.py` implements the MCP **streamable HTTP** protocol directly on Lambda:

- `POST` with a JSON-RPC body; notifications return `202`, requests return `200`.
- If the client sends `Accept: application/json` it receives a single JSON-RPC response.
- If the client sends `Accept: text/event-stream` it receives the same response wrapped in one SSE `message` event, so streaming-capable clients work without requiring API Gateway's response-streaming features.
- `OPTIONS` preflight plus plain API-Gateway v1/v2 proxy and Lambda-Function-URL events are handled by the same normalizer.

No long-lived connections are needed, which is exactly what makes this Lambda-friendly.

## Security notes

- The Function URL is `AuthType: AWS_IAM` — every request must be SigV4-signed
  with the `biz-mcp-client` IAM user's keys. Only that user can invoke it;
  anonymous requests get `403 Forbidden`.
- The client access key/secret are printed once in the stack outputs after
  deploy. Keep them safe: the bundled agent stores them in a gitignored
  `agent/.env` (or Windows user env vars). If leaked, rotate them (create a new
  access key for the user and delete the old one).
- CoinGecko and Frankfurter free tiers have rate limits; failures surface as tool errors.

## Customization

- Add tools: define a function in `mcp_server.py` with the `@mcp.tool()` decorator and a docstring; the JSON schema is generated automatically. Rebuild/`sam deploy` to publish.
- Note: `mcp` is pinned to `>=1.30,<2` because mcp 2.x hard-depends on `pywin32` on Windows, which breaks `sam build` on Windows hosts. The API used here is identical in v2 (just import `MCPServer` instead of `FastMCP`).
- Swap the store: keep `db.py`'s interface (`save_item`, `get_item`, `list_items`) and implement a new backend (e.g. RDS, S3).
- Switch transport: the built-in `mcp.run(transport="streamable-http")` path runs the same tools as a plain web server if you ever move to EC2 instead.