Skip to main content
Glama
rezapars

Client Onboarding MCP

by rezapars
README.md
# Client Onboarding MCP for Microsoft Copilot and Teams

Minimal Python 3.12 project with:

- MCP server with client-session authentication and five tools
- Separate mock backend server
- REST/OpenAPI facade
- Microsoft 365 Copilot plugin package files
- Microsoft Teams app manifest
- Docker, tests, local setup, auth placeholders, and examples

Architecture:

```text
[Microsoft Teams]
        |
        v
[Microsoft Copilot]
        |
        v
[MCP Server]
        |
        v
[Mock Backend Server]
```

## Folder Tree

```text
.
|-- appPackage/
|   |-- adaptive-cards/
|   |   `-- client-result.json
|   |-- color.png
|   |-- declarativeAgent.json
|   |-- manifest.json
|   |-- mcp-tools.json
|   |-- openapi.yaml
|   |-- outline.png
|   |-- plugin.json
|   `-- plugin.openapi.json
|-- examples/
|   |-- curl.md
|   |-- json-rpc.md
|   |-- requests.http
|   `-- responses/
|       |-- basic_info_family.json
|       |-- basic_info_name.json
|       |-- auth_failed.json
|       |-- auth_required.json
|       |-- auth_success.json
|       |-- error_invalid_field.json
|       |-- facility_limit.json
|       |-- onboarding_status.json
|       `-- outreach_summary.json
|-- mcp_server/
|   |-- __init__.py
|   |-- __main__.py
|   |-- app.py
|   |-- auth.py
|   |-- backend_client.py
|   |-- config.py
|   |-- errors.py
|   |-- jsonrpc.py
|   |-- logging_config.py
|   |-- mcp_app.py
|   |-- models.py
|   |-- routes.py
|   |-- session.py
|   `-- tool_registry.py
|-- mock_backend/
|   |-- __init__.py
|   |-- __main__.py
|   |-- app.py
|   |-- config.py
|   |-- logging_config.py
|   |-- models.py
|   `-- routes.py
|-- tests/
|   |-- conftest.py
|   |-- test_backend.py
|   |-- test_mcp_http.py
|   `-- test_tool_registry.py
|-- .env.example
|-- .vscode/launch.json
|-- Dockerfile
|-- docker-compose.yml
|-- main.py
|-- openapi.yaml
|-- pytest.ini
|-- requirements.txt
`-- tools.json
```

## Tools

The MCP exposes these five tools.

### authenticate_client

Input:

```json
{
  "client_id": "123",
  "otp": "9632"
}
```

Successful output:

```json
{
  "authenticated": true,
  "client_id": "123",
  "message": "Authentication successful"
}
```

Failed output:

```json
{
  "authenticated": false,
  "message": "Invalid OTP"
}
```

Business tools use the authenticated `client_id` stored in the MCP session. For local HTTP tests, keep the same `x-mcp-session-id` header across calls.

### get_client_onboarding_status

Input:

```json
{}
```

Output:

```json
{
  "client_id": "123",
  "status": "Completed",
  "message": "Client onboarding completed successfully"
}
```

### get_client_basic_info

Input:

```json
{
  "field": "name"
}
```

Allowed `field` values:

- `name`
- `family`

Output:

```json
{
  "client_id": "123",
  "field": "name",
  "value": "John"
}
```

```json
{
  "client_id": "123",
  "field": "family",
  "value": "Doe"
}
```

### get_client_facility_limit

Input:

```json
{}
```

Output:

```json
{
  "client_id": "123",
  "client_name": "John Doe",
  "facility_limit_eur": 75000000,
  "currency": "EUR",
  "formatted_limit": "EUR 75,000,000"
}
```

### summarize_client_outreach

Input:

```json
{}
```

Output includes the outreach reasons, highlights, and total questions to answer:

```json
{
  "client_id": "123",
  "client_name": "John Doe",
  "outreach_count": 2,
  "questions_to_answer_count": 5,
  "reasons": ["Annual facility review", "Updated cash-flow forecast"],
  "summary": "John Doe has 2 outreach items. Reasons: Annual facility review, Updated cash-flow forecast. You should answer 5 questions in total."
}
```

## Local Setup

Use Python 3.12.

```bash
python -m venv .venv312
.venv312\Scripts\activate
pip install -r requirements.txt
copy .env.example .env
```

Terminal 1:

```bash
uvicorn mock_backend.app:app --host 0.0.0.0 --port 8001 --reload
```

Terminal 2:

```bash
uvicorn mcp_server.app:app --host 0.0.0.0 --port 8000 --reload
```

Health checks:

```bash
curl http://localhost:8001/health
curl http://localhost:8000/health
```

## REST API

OpenAPI 3.1 schema:

- Static YAML: `openapi.yaml`
- Runtime JSON: `http://localhost:8000/openapi.json`
- Runtime YAML file endpoint: `http://localhost:8000/openapi.yaml`

Endpoints:

```bash
curl -X POST http://localhost:8000/api/v1/authenticate \
  -H "Content-Type: application/json" \
  -H "x-mcp-session-id: demo-session" \
  -d '{"client_id":"123","otp":"9632"}'
curl http://localhost:8000/api/v1/client/onboarding-status -H "x-mcp-session-id: demo-session"
curl http://localhost:8000/api/v1/client/basic-info/name -H "x-mcp-session-id: demo-session"
curl http://localhost:8000/api/v1/client/facility -H "x-mcp-session-id: demo-session"
curl http://localhost:8000/api/v1/client/outreach-summary -H "x-mcp-session-id: demo-session"
curl http://localhost:8000/api/v1/clients/123/onboarding-status
curl http://localhost:8000/api/v1/clients/123/basic-info/name
curl http://localhost:8000/api/v1/clients/123/basic-info/family
curl http://localhost:8000/api/v1/clients/123/facility
curl http://localhost:8000/api/v1/clients/123/outreach-summary
```

## MCP JSON-RPC

List tools:

```bash
curl -X POST http://localhost:8000/mcp ^
  -H "Content-Type: application/json" ^
  -H "x-mcp-session-id: demo-session" ^
  -d "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"tools/list\"}"
```

Authenticate first:

```bash
curl -X POST http://localhost:8000/mcp ^
  -H "Content-Type: application/json" ^
  -H "x-mcp-session-id: demo-session" ^
  -d "{\"jsonrpc\":\"2.0\",\"id\":2,\"method\":\"tools/call\",\"params\":{\"name\":\"authenticate_client\",\"arguments\":{\"client_id\":\"123\",\"otp\":\"9632\"}}}"
```

Then call business tools with the same session ID and no `client_id` argument:

```bash
curl -X POST http://localhost:8000/mcp ^
  -H "Content-Type: application/json" ^
  -H "x-mcp-session-id: demo-session" ^
  -d "{\"jsonrpc\":\"2.0\",\"id\":3,\"method\":\"tools/call\",\"params\":{\"name\":\"get_client_onboarding_status\",\"arguments\":{}}}"
```

The MCP Python SDK registration is in `mcp_server/mcp_app.py`. For stdio-style MCP testing:

```bash
python -m mcp_server.mcp_app
```

## Mock Backend

The mock backend is a separate FastAPI service and returns hardcoded data only.

```bash
curl http://localhost:8001/status/123
curl http://localhost:8001/info/123/name
curl http://localhost:8001/info/123/family
curl http://localhost:8001/facility/123
curl http://localhost:8001/outreach/123/summary
```

## MCP Client Session Authentication

MCP client-session authentication is enabled in the tool flow:

- Tool: `authenticate_client`
- Required input: `client_id`, `otp`
- Demo OTP: `9632`
- Storage: in-memory only
- Session key: `x-mcp-session-id` or `mcp-session-id` header for HTTP tests

If a business tool is called before authentication, the MCP tool returns:

```json
{
  "error": "AUTH_REQUIRED",
  "message": "Please authenticate first using client_id and OTP"
}
```

Production replacement points are marked in [session.py](C:/Users/shoja/PycharmProjects/MCP/mcp_server/session.py):

- Replace the hardcoded OTP with real OTP validation.
- Load client/user mappings from a database.
- Bind session IDs to Microsoft Entra ID or Teams user identity.
- Store sessions in Redis or another shared session store.

## API Key Placeholder

API key validation is intentionally disabled for local development. This is separate from the MCP client-session authentication tool.

Enable API key validation:

```env
MCP_AUTH_REQUIRED=true
MCP_API_KEY_HEADER=x-api-key
MCP_API_KEY=local-dev-key
```

Then call:

```bash
curl http://localhost:8000/api/v1/clients/123/onboarding-status -H "x-api-key: local-dev-key"
```

Production replacement points:

- Replace `APIKeyAuthMiddleware` with Microsoft Entra ID JWT validation.
- Register an app in Microsoft Entra ID.
- Configure Teams bot and Copilot plugin auth references with the production app registration.
- Store secrets in Azure Key Vault or a managed secret store.

## Docker

```bash
docker compose up --build
```

Services:

- MCP server: `http://localhost:8000`
- Mock backend: `http://localhost:8001`

## Copilot Package

Package files are in `appPackage/`.

- `manifest.json`: Microsoft 365/Teams app manifest
- `declarativeAgent.json`: declarative agent manifest
- `plugin.json`: Microsoft 365 Copilot plugin manifest using `RemoteMCPServer`
- `plugin.openapi.json`: OpenAPI fallback plugin manifest
- `mcp-tools.json`: MCP tool discovery metadata mirror of the inline plugin metadata
- `openapi.yaml`: OpenAPI schema for the REST facade
- `color.png` and `outline.png`: Teams package icons

Before packaging:

1. Replace the hosted domain in `appPackage/plugin.json`, `appPackage/openapi.yaml`, and `appPackage/manifest.json` if you move away from the current Railway URL.
2. Replace the app ID in `appPackage/manifest.json` with a real package ID before production publishing.
3. Zip the contents of `appPackage/`, not the folder itself.
4. Upload the zip in Teams Developer Portal or Microsoft 365 Agents Toolkit.

Example Copilot prompts:

- Authenticate client 123 with OTP 9632
- Get my onboarding status
- What is the client's name?
- What is the client's family name?
- What is the current limit on my company's facility?
- Summarize my outreach highlights

## Teams Local Testing

1. Start both local services.
2. Start an HTTPS tunnel:

```bash
ngrok http 8000
```

3. Copy the HTTPS ngrok domain, for example `https://abc123.ngrok-free.app`.
4. Replace the hosted domain in `appPackage/manifest.json`, `appPackage/plugin.json`, and `appPackage/openapi.yaml`.
5. In Teams Developer Portal, import the app package zip.
6. Validate the manifest and test in a personal scope first.

This project is packaged as a Copilot declarative agent. It does not implement a Bot Framework message handler.

## Error Handling Examples

Invalid basic info field through REST:

```bash
curl http://localhost:8000/api/v1/clients/123/basic-info/age
```

Response shape:

```json
{
  "error": {
    "code": "validation_error",
    "message": "Request validation failed.",
    "details": []
  }
}
```

Invalid MCP tool call:

```json
{
  "jsonrpc": "2.0",
  "id": 3,
  "method": "tools/call",
  "params": {
    "name": "get_client_basic_info",
    "arguments": {
      "field": "age"
    }
  }
}
```

Response shape:

```json
{
  "jsonrpc": "2.0",
  "id": 3,
  "error": {
    "code": -32602,
    "message": "Tool arguments failed validation.",
    "data": {
      "code": "invalid_arguments",
      "details": []
    }
  }
}
```

## Logging

Both services log requests with method, path, status code, and elapsed time.

Log levels:

```env
MCP_LOG_LEVEL=INFO
BACKEND_LOG_LEVEL=INFO
```

## Tests

```bash
pytest -q
```

Tests cover:

- Mock backend responses
- REST facade responses
- MCP `tools/list`
- MCP `tools/call`
- Tool validation

## API Versioning Strategy

REST endpoints are versioned under `/api/v1`.

Recommended evolution:

- Keep `/api/v1` stable for existing Copilot plugins.
- Add `/api/v2` for breaking response or parameter changes.
- Keep MCP tool names stable where possible.
- Add new tool names for breaking tool contract changes.
- Keep `operationId` values aligned with Copilot plugin function names.

## References

- Microsoft 365 Copilot plugins support REST APIs and MCP servers through declarative agent actions.
- Current Microsoft plugin manifest schema version used here: `v2.4`.
- Current Microsoft 365 app manifest schema used here: `v1.22`.