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** (public, HTTPS, `AuthType: NONE`) 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,
- Lambda `url:Invoke` and `Function:Invoke` permissions (both are required since Oct 2025 for Function URLs with `AuthType: NONE`).

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`.

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
```

### Point an MCP client at it

Claude Desktop (`claude_desktop_config.json`):

```json
{
  "mcpServers": {
    "biz-mcp": {
      "type": "http",
      "url": "https://YOUR_ENDPOINT/"
    }
  }
}
```

The MCP Inspector also accepts an HTTP server URL directly.

## Run the bundled agent (free Groq brain)

`agent/agent.py` is a ready-made agent that connects to the MCP endpoint and uses a
**free-tier Groq** model to drive the 7 tools with 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_..."

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

```powershell
curl -s -X POST https://YOUR_ENDPOINT/ `
  -H "Content-Type: application/json" `
  -H "Accept: application/json, text/event-stream" `
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18"}}'
```

`tools/list` will return the 7 tools:

```powershell
curl -s -X POST https://YOUR_ENDPOINT/ `
  -H "Content-Type: application/json" `
  -d '{"jsonrpc":"2.0","id":2,"method":"tools/list"}'
```

## 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 (read before going public)

- The Function URL is `AuthType: NONE`, meaning **anyone with the URL can invoke the Lambda**.
- Each public call invokes billing and lets anyone read/write the DynamoDB table.
- For production: set `AuthType: AWS_IAM` and use a SigV4-signed MCP client, add request throttling, or front the URL with WAF. The tools also accept arbitrary `pk`/`sk`, so add an authorization layer (e.g. per-tenant key prefix) if the data is sensitive.
- 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.