Skip to main content
Glama
franciscomartinezfc

procurement-mailer-mcp

README.md
# procurement-mailer-mcp

A [Model Context Protocol](https://modelcontextprotocol.io) server that lets an **SAP Joule** agent read and send email from a scoped Microsoft Exchange mailbox. The agent never talks to Microsoft directly โ€” this server sits in the middle, translating MCP tool calls into Microsoft Graph requests against a single mailbox.

๐Ÿ“– **Full setup runbook:** https://franciscomartinezfc.github.io/procurement-mailer-mcp/

## Architecture

```
  SAP Joule Agent  โ”€โ”€โ–ถ  procurement-mailer-mcp  โ”€โ”€โ–ถ  Exchange / MS Graph
   (BTP / Joule)         (this repo, on CF)          (single scoped mailbox)
```

Invocation flows left-to-right; data flows back right-to-left. Three stages:

| Stage | What it does |
|-------|--------------|
| **1 ยท Joule** | Plans the request, picks an MCP tool, calls the server over **Streamable HTTP** through a BTP destination. |
| **2 ยท MCP** (this repo) | Node.js + TypeScript server on Cloud Foundry. Exposes four tools and maps them onto Graph calls through the `graph-mail` destination (an Entra app identity). |
| **3 ยท Graph** | Microsoft Graph over Exchange Online, restricted by an application access policy to just the demo mailbox. |

### Transport

The server is **stateless**: every `POST /mcp` builds a fresh `McpServer` + `StreamableHTTPServerTransport` pair (`sessionIdGenerator: undefined`). `GET` and `DELETE` on `/mcp` โ€” the session channels of Streamable HTTP โ€” return `405`.

### Graph access

There is no MSAL or token juggling in code. `@sap-cloud-sdk/http-client`'s `executeHttpRequest` resolves the `graph-mail` BTP destination, performs the OAuth client-credentials exchange, and prefixes `/v1.0` on every call. The Entra credentials (tenant ID, client ID, client secret) live in that **destination**, not in environment variables.

## Project layout

```
src/
โ”œโ”€โ”€ index.ts          # Express app + Streamable HTTP transport (POST /mcp)
โ”œโ”€โ”€ server.ts         # Builds the McpServer, registers `ping`, wires in mail tools
โ”œโ”€โ”€ tools/
โ”‚   โ””โ”€โ”€ mail.ts       # The three mail tools: list_messages, read_message, send_mail
โ””โ”€โ”€ graph/
    โ””โ”€โ”€ client.ts     # graphGet / graphPost via the `graph-mail` destination

manifest.yml          # Cloud Foundry app + service bindings (dest, conn, xsuaa)
index.html            # The setup runbook (published to GitHub Pages)
assets/               # Runbook images
```

## Tools

| Tool | Description | Key inputs |
|------|-------------|------------|
| `ping` | Health check โ€” returns `pong @ <timestamp>`. | `note?` |
| `list_messages` | Recent emails from the mailbox: id, subject, sender, received time, read state. | `top` (1โ€“25, default 10), `unreadOnly` |
| `read_message` | Full body of one email by id. | `messageId` |
| `send_mail` | Send an email from the mailbox. | `to`, `subject`, `body`, `contentType` (`Text`/`HTML`) |

## Requirements

- **Node.js 22.x** (see `engines` in `package.json`)
- For deployment: a **SAP BTP** subaccount with Cloud Foundry, and the `graph-mail` destination configured (Entra credentials). See the [runbook](https://franciscomartinezfc.github.io/procurement-mailer-mcp/) for the full Microsoft-side and BTP setup.

## Run locally

```bash
npm install

# Dev โ€” tsx watch, hot reload, port 3000 (or $PORT)
npm run dev

# Or build to dist/ and run the compiled server
npm run build
npm start
```

The server listens on `http://localhost:3000/mcp`.

### Validate with the MCP Inspector

Use a real MCP client โ€” **not `curl`**, which can't complete the Streamable HTTP handshake.

```bash
npx @modelcontextprotocol/inspector
# Transport: Streamable HTTP   URL: http://localhost:3000/mcp
# Connect โ†’ List Tools (expect 4) โ†’ call `ping` โ†’ expect "pong @ <timestamp>"
```

> `ping` works locally with no extra setup. The three mail tools resolve the `graph-mail`
> destination, which only exists on BTP โ€” test those against the deployed route, or feed the
> SAP Cloud SDK a `destinations` env var when starting the server locally.

## Deploy to Cloud Foundry

The app expects three bound services (see `manifest.yml`): a `destination`, `connectivity`, and `xsuaa` instance.

```bash
cf api <CF_API_ENDPOINT>          # e.g. https://api.cf.us10-001.hana.ondemand.com
cf login --sso
cf target -o "<ORG>" -s <SPACE>

# One-time: create the backing services manifest.yml binds
cf create-service destination lite  procurement-mailer-dest
cf create-service connectivity lite procurement-mailer-conn
cf create-service xsuaa application procurement-mailer-uaa

npm run build
cf push

# After re-binding or changing a service: RESTAGE, never restart
cf restage procurement-mailer-mcp
```

> **Restage, never restart.** Service bindings are only injected into `VCAP_SERVICES` on
> `cf restage`. `cf restart` reuses the old droplet and the env stays empty.

## Configuration

| Setting | Where | Notes |
|---------|-------|-------|
| `MAILBOX` | `manifest.yml` env / `src/graph/client.ts` default | The mailbox every call targets. |
| `PORT` | injected by Cloud Foundry | Defaults to `3000` locally. |
| Entra credentials | `graph-mail` BTP destination | Tenant ID, client ID, client secret โ€” **not** env vars. |

## License

ISC