Skip to main content
Glama
README.md

# MCP Server Demo

A demonstration [Model Context Protocol (MCP)](https://modelcontextprotocol.io) server
built with **FastMCP**. It exposes a small set of tools, resources, and prompts over the
`streamable-http` transport and secures every request with **Microsoft Entra ID (OAuth 2.0)**.

The server is designed to run both locally (developer machine) and in Azure
(App Service / Container Apps), and it can call downstream Azure services such as
Azure Data Explorer (Kusto) **as the calling user** using the On-Behalf-Of (OBO) flow
backed by a federated managed identity.

---

## What this project does

At a high level, the server lets an MCP client (e.g. VS Code + GitHub Copilot in agent
mode) discover and invoke server-side capabilities after the user signs in with their
Microsoft Entra account:

- **Tools** — callable functions the model can invoke:
  - `get_weather(location)` — current weather for a city/place via the public Open-Meteo API.
  - `get_ip_address()` — the server's public IP via ipify.
  - `execute_kql(query, cluster?, database?)` — runs a Kusto (KQL) query against Azure
    Data Explorer and returns JSON. Enforces a 1000-row safety cap and runs the query
    under the caller's identity when Azure-hosted (OBO).
  - `add(a, b)` — trivial example tool.
  - `long_running_task(task_name, steps)` — demonstrates progress reporting via the MCP `Context`.
- **Resource** — `greeting://{name}` returns a personalized greeting (dynamic resource template).
- **Prompt** — `greet_user(name, style)` returns a reusable greeting prompt.
- **Routes** — `/mcp` (the MCP endpoint) plus a public `/readme` HTML route and the
  OAuth discovery endpoint `/.well-known/oauth-protected-resource`.

---

## Architecture

### Component layout

```
main.py                 # Server entry point: builds FastMCP, wires auth, registers tools, serves via uvicorn
client.py               # Demo MCP client (acquires a token with DefaultAzureCredential, calls the server)
auth/
  authCheck.py          # EntraTokenVerifier: validates incoming JWTs (signature, issuer, audience, scope/role)
tools/
  weather.py            # register_weather_tools -> get_weather
  findIP.py             # register_ip_tools -> get_ip_address
  executeKQL.py         # register_kql_tools -> execute_kql (OBO + federated managed identity)
GoogleCalendar.py       # Standalone Google Calendar experiment (not wired into the MCP server)
AUTH.md                 # Deep-dive on the Entra ID OAuth setup and gotchas
pyproject.toml /        # Dependencies (mcp[cli], azure-identity, azure-kusto-data, pyjwt[crypto], starlette, uvicorn, ...)
requirements.txt
```

### Request flow

```mermaid
sequenceDiagram
    actor User
    participant Client as MCP Client (VS Code / client.py)
    participant Entra as Microsoft Entra ID
    participant Server as FastMCP Server (/mcp)
    participant Verifier as EntraTokenVerifier
    participant Kusto as Azure Data Explorer

    Client->>Server: GET /.well-known/oauth-protected-resource
    Server-->>Client: issuer + scopes_supported (api://<app>/mcp.access)
    Client->>Entra: Request token for api://<app>/mcp.access
    Entra->>User: Sign in / consent
    Entra-->>Client: Access token (aud = API, scp = mcp.access)
    Client->>Server: MCP request + Bearer token
    Server->>Verifier: verify_token(token)
    Verifier->>Entra: Fetch JWKS signing keys (cached)
    Verifier-->>Server: AccessToken (valid) or reject (401/403)
    Server->>Kusto: execute_kql via OBO (run as caller, Azure-hosted)
    Kusto-->>Server: Result rows
    Server-->>Client: Tool result
```

### Key design points

- **FastMCP + streamable-http.** OAuth requires the `streamable-http` transport
  (`stdio` cannot do OAuth). The server is built in [main.py](main.py) and served by
  `uvicorn` on `0.0.0.0`, reading `PORT` from the environment (App Service injects it).
- **Modular tool registration.** Each tool group lives in its own module under
  [tools/](tools) and exposes a `register_*_tools(mcp)` function, keeping `main.py`
  thin and tools independently testable.
- **Per-user downstream calls (OBO).** [tools/executeKQL.py](tools/executeKQL.py)
  reads the caller's validated token via `get_access_token()` and, when running in
  Azure, exchanges it through an `OnBehalfOfCredential` whose client assertion is a
  **federated managed identity** (no secret/cert). Locally it falls back to
  `AzureCliCredential` (`az login`) then interactive browser sign-in.
- **Safety guardrails.** `execute_kql` refuses to return result sets larger than
  `MAX_ROWS` (1000) and asks the caller to aggregate or filter instead.

---

## Authentication (brief)

Authentication is implemented with **Microsoft Entra ID** using OAuth 2.0 bearer
tokens. Full details and troubleshooting live in [AUTH.md](AUTH.md); the essentials:

1. **Server advertises a protected resource.** `AuthSettings` in [main.py](main.py)
   publishes the issuer (`https://login.microsoftonline.com/<tenant>/v2.0`) and the
   **fully-qualified** required scope `api://<api-app-id>/mcp.access`. The scope must be
   fully-qualified — a bare `mcp.access` is interpreted by Entra as a Microsoft Graph
   scope and fails with `AADSTS65002`.

2. **Token validation** happens in `EntraTokenVerifier`
   ([auth/authCheck.py](auth/authCheck.py)) for every request:
   - Verifies the JWT signature against Entra's JWKS keys (`PyJWKClient`, RS256).
   - Accepts both **v1** (`sts.windows.net/<tid>/`) and **v2**
     (`login.microsoftonline.com/<tid>/v2.0`) issuers.
   - Accepts both audience forms: `api://<guid>` and the bare `<guid>`.
   - Authorizes either a **delegated user** (`mcp.access` in the `scp` claim) **or** an
     **app-only caller** (`mcpaccess.app` in the `roles` claim, e.g. a managed identity).
   - Re-adds the fully-qualified scope to the returned `AccessToken.scopes` so the MCP
     bearer middleware's `required_scopes` check passes.

3. **Client sign-in options:**
   - **VS Code native sign-in** — VS Code reads the discovery endpoint and acquires a
     token through its built-in Microsoft auth provider; no token handling code needed.
   - **Manual bearer token** — `az account get-access-token --scope "api://<app>/.default"`
     and paste it into `.vscode/mcp.json` as an `Authorization` header.
   - **Programmatic** — [client.py](client.py) uses `DefaultAzureCredential`
     (`az login` locally, managed identity in Azure).

One-time Entra setup (app registration, *Expose an API*, and pre-authorizing the VS Code
and Azure CLI first-party client IDs) is documented in [AUTH.md](AUTH.md).

---

## Running locally

1. Create a `.env` file with your Entra values:

   ```dotenv
   ENTRA_TENANT_ID=<your-tenant-id>
   ENTRA_AUDIENCE=api://<api-app-id>
   ENTRA_CLIENT_ID=<client-app-id>
   # Optional, used by execute_kql:
   KUSTO_CLUSTER=https://<cluster>.kusto.windows.net
   KUSTO_DATABASE=<database>
   # Set when deployed:
   # RESOURCE_SERVER_URL=https://<app>.azurewebsites.net
   ```

2. Install dependencies and start the server:

   ```powershell
   pip install -r requirements.txt
   python main.py
   ```

   The MCP endpoint is served at `http://localhost:8000/mcp`.

3. Verify the advertised OAuth metadata:

   ```powershell
   curl.exe -s http://localhost:8000/.well-known/oauth-protected-resource
   ```

4. (Optional) Exercise the server with the demo client:

   ```powershell
   az login --tenant <your-tenant-id>
   python client.py
   ```

---

## Notes

- `main.py` includes verbose `print` statements that are useful for learning/debugging
  the auth flow; replace them with structured logging for production use.