basic-mcp-server
by kkmalik89
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.This server cannot be deployed
Maintenance
ActivityInactive
ResponsivenessNo issues