Metadata-Version: 2.4
Name: fca-mcp
Version: 0.0.4
Summary: MCP server for the UK Financial Services (FCA) Register RESTful API
Author-Email: "I. Orlovs" <ilja@release.art>
Maintainer-Email: "I. Orlovs" <ilja@release.art>
License-Expression: MIT
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Internet :: WWW/HTTP :: HTTP Servers
Classifier: Topic :: Office/Business :: Financial
Classifier: Typing :: Typed
Project-URL: Homepage, https://docs.release.art/fca-api/
Project-URL: Documentation, https://docs.release.art/fca-api/
Project-URL: Repository, https://github.com/release-art/fca-api/
Requires-Python: >=3.13
Requires-Dist: pydantic>=2.12.5
Requires-Dist: uvicorn[standard]>=0.32.0
Requires-Dist: typer>=0.21.2
Requires-Dist: fca-api>=1.2.0
Requires-Dist: python-toon>=0.1.3
Requires-Dist: fastmcp<4,>=3
Requires-Dist: asyncstdlib>=3.13.1
Requires-Dist: pydantic-settings>=2.13.1
Requires-Dist: azure-data-tables>=12.7.0
Requires-Dist: azure-storage-blob>=12.28.0
Requires-Dist: py-key-value-aio>=0.4.4
Requires-Dist: azure-storage-queue>=12.15.0
Requires-Dist: aiohttp>=3.13.3
Requires-Dist: azure-identity>=1.25.2
Requires-Dist: Jinja2>=3.1.6
Requires-Dist: pyjwt>=2.12.1
Requires-Dist: opentelemetry-sdk>=1.41.0
Requires-Dist: opentelemetry-exporter-otlp-proto-http>=1.41.0
Requires-Dist: opentelemetry-instrumentation-starlette>=0.62b0
Requires-Dist: opentelemetry-instrumentation-httpx>=0.62b0
Requires-Dist: opentelemetry-instrumentation-logging>=0.62b0
Requires-Dist: python-json-logger>=4.1.0
Provides-Extra: user
Description-Content-Type: text/markdown

# FCA Register MCP Server

[![CI](https://github.com/release-art/fca-mcp/actions/workflows/ci.yml/badge.svg)](https://github.com/release-art/fca-mcp/actions/workflows/ci.yml)
[![License: MIT](https://img.shields.io/badge/License-MIT-brightgreen.svg)](https://opensource.org/licenses/MIT)
[![PyPI version](https://img.shields.io/pypi/v/fca-mcp?logo=python&color=41bb13)](https://pypi.org/project/fca-mcp)

A [Model Context Protocol](https://modelcontextprotocol.io/) server that exposes the UK [FCA Financial Services Register](https://register.fca.org.uk/) to LLM clients as a set of read-only tools. Built on [FastMCP v3](https://gofastmcp.com/) and the [`fca-api`](https://github.com/release-art/fca-api) async client.

## Overview

- **25 read-only tools** across five domains: firms, individuals, funds, regulated markets, and identifier search.
- **Two transports**: HTTP (FastAPI/uvicorn) for remote MCP clients, and stdio for local integrations.
- **OAuth2 via Auth0**, with two modes:
  - `remote` — JWT verification only (the MCP server trusts an upstream Auth0 tenant).
  - `proxy` — full OAuth proxy with dynamic client registration; tokens are persisted to Azure Blob Storage, encrypted with Fernet.
- **Scope-based authorization**: tools tagged `fca_api:read` require the `fca-api:read` scope in the access token. Enforcement is per-tool, so `initialize` and `tools/list` remain reachable by unauthenticated clients.
- **Structured responses**: Pydantic models generated by reflection from `fca-api` types, stripping internal fields.

## Tools

| Module | Tools |
|--------|-------|
| [`search.py`](src/fca_mcp/server/search.py) | `search_frn`, `search_irn`, `search_prn` |
| [`firms.py`](src/fca_mcp/server/firms.py) | `get_firm`, `get_firm_names`, `get_firm_addresses`, `get_firm_controlled_functions`, `get_firm_individuals`, `get_firm_permissions`, `get_firm_requirements`, `get_firm_requirement_investment_types`, `get_firm_regulators`, `get_firm_passports`, `get_firm_passport_permissions`, `get_firm_waivers`, `get_firm_exclusions`, `get_firm_disciplinary_history`, `get_firm_appointed_representatives` |
| [`individuals.py`](src/fca_mcp/server/individuals.py) | `get_individual`, `get_individual_controlled_functions`, `get_individual_disciplinary_history` |
| [`funds.py`](src/fca_mcp/server/funds.py) | `get_fund`, `get_fund_names`, `get_fund_subfunds` |
| [`markets.py`](src/fca_mcp/server/markets.py) | `get_regulated_markets` |

All tools are decorated with `ToolAnnotations(readOnlyHint=True, destructiveHint=False, idempotentHint=True, openWorldHint=True)`.

## Quick start

### Prerequisites

- Python **3.13+**
- [PDM](https://pdm-project.org/) for dependency management
- FCA Register API credentials (`FCA_API_USERNAME`, `FCA_API_KEY`)
- An Auth0 tenant (only the `remote` mode needs a tenant at minimum; `proxy` mode additionally needs client credentials and Azure Blob Storage)

### Install

```bash
pdm install
cp .env.example .env
# edit .env with your credentials
```

### Run the HTTP server

```bash
pdm run python -m fca_mcp serve                 # production-like
pdm run python -m fca_mcp serve --reload        # with autoreload
```

By default the server binds `0.0.0.0:8000`. `SERVER_BASE_URL` must be set (used for OAuth metadata and resource URLs).

### Run over stdio

```bash
pdm run python -m fca_mcp stdio
```

`stdio` mode skips the `AuthMiddleware` entirely — see [`server/__init__.py`](src/fca_mcp/server/__init__.py).

### Docker

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

`docker-compose.yml` launches the server alongside an [Azurite](https://learn.microsoft.com/azure/storage/common/storage-use-azurite) emulator. Set `FCA_API_USERNAME`, `FCA_API_KEY`, `AUTH0_DOMAIN`, `AUTH0_AUDIENCE`, and `SERVER_BASE_URL` in the environment of the host running `docker-compose`.

## Architecture

```
LLM client
  │  MCP over HTTP (OAuth2 Bearer) or stdio
  ▼
┌─────────────────────────────────────────────┐
│ FastMCP server (fca_mcp.server.get_server)  │
│ ─────────────────────────────────────────── │
│  Middleware stack (outer → inner):          │
│    ErrorHandlingMiddleware                  │
│    RateLimitingMiddleware                   │
│    LoggingMiddleware                        │
│    AuthMiddleware (restrict_tag FCA_API_RO) │
│ ─────────────────────────────────────────── │
│  Sub-servers (mounted):                     │
│    search · firms · funds ·                 │
│    individuals · markets                    │
│ ─────────────────────────────────────────── │
│  Auth provider:                             │
│    RemoteAuthProvider  (mode=remote)        │
│    Auth0Provider       (mode=proxy)         │
└───────────────────────┬─────────────────────┘
                        ▼
            fca_api.async_api.Client
                        ▼
               FCA Register REST API
```

### Dependency injection

Tools receive the shared `fca_api` client through FastMCP's `Depends` chain defined in [`server/deps.py`](src/fca_mcp/server/deps.py):

```python
async def get_firm(
    frn: FrnParam,
    fca_client: fca_api.async_api.Client = deps.FcaApiDep,
) -> types.firm.FirmDetails:
    result = await fca_client.get_firm(frn)
    return types.firm.FirmDetails.from_api_t(result)
```

The client is constructed once in the server lifespan and reused across requests.

### Type reflection

Response types are synthesised from `fca-api` types by `reflect_fca_api_t()` in [`server/types/base.py`](src/fca_mcp/server/types/base.py). Fields marked with `InternalUrl` are stripped. Tools convert raw results with `Model.from_api_t(api_result)`.

### Auth: scopes vs tags

Note the hyphen-vs-underscore distinction:

- **Scope** ([`auth/scopes.py`](src/fca_mcp/server/auth/scopes.py)): `FCA_API_RO = "fca-api:read"` — the OAuth scope claimed in access tokens.
- **Tag** ([`auth/tags.py`](src/fca_mcp/server/auth/tags.py)): `FCA_API_RO = "fca_api:read"` — the tag applied to tool decorators.

`AuthMiddleware` calls `restrict_tag(FCA_API_RO, scopes=[FCA_API_RO])` — any tool tagged with `fca_api:read` requires the `fca-api:read` scope to execute. Untagged tools are allowed through without scope checks. On `stdio`, the middleware short-circuits.

## Configuration

All settings are loaded from environment variables via Pydantic-Settings. See [SETTINGS.md](SETTINGS.md) for the full reference and [`.env.example`](.env.example) for a working template.

The most important variables:

| Variable | Required | Purpose |
|----------|----------|---------|
| `FCA_API_USERNAME` / `FCA_API_KEY` | Yes | FCA Register API credentials |
| `AUTH0_MODE` | No (`remote` default) | `remote` for JWT-only, `proxy` for OAuth proxy |
| `AUTH0_DOMAIN` / `AUTH0_AUDIENCE` | Yes | Auth0 tenant identifiers |
| `AUTH0_CLIENT_ID` / `AUTH0_CLIENT_SECRET` / `AUTH0_JWT_SIGNING_KEY` / `AUTH0_STORAGE_ENCRYPTION_KEY` | `proxy` mode only | OAuth proxy secrets |
| `AUTH0_INTERACTIVE_CLIENT_ID` | No | When set, exposes the interactive `/interactive` web UI |
| `AZURE_CREDENTIAL` | `proxy` mode | `none` (connection string / Azurite) or `default` (DefaultAzureCredential) |
| `AZURE_STORAGE_CONNECTION_STRING` | When `AZURE_CREDENTIAL=none` | Connection string for Azurite or an Azure Storage account |
| `AZURE_STORAGE_ACCOUNT` | When `AZURE_CREDENTIAL=default` | Storage account name |
| `SERVER_BASE_URL` | Yes | Public base URL used in OAuth resource metadata |

## Health check

The HTTP app exposes `GET /.container/health`, returning service name, version, uptime, and timestamp. The Dockerfile `HEALTHCHECK` polls this endpoint.

## Development

```bash
pdm run pytest                                      # all tests with coverage
pdm run pytest tests/server/test_firm_simple.py     # single file
pdm run pytest tests/server/test_firm_simple.py::test_get_firm -v

pdm run ruff check                                  # lint
pdm run ruff check --fix                            # auto-fix
pdm run ruff format                                 # format
```

Tests use FastMCP's in-memory `FastMCPTransport` — no HTTP server or live Auth0/Azure is required. Fixtures in [`tests/server/conftest.py`](tests/server/conftest.py) replace the `fca-api` client with a record/replay mock and substitute a synthetic `AccessToken` so the auth middleware runs without live tokens. To test scope denial, override the `oauth_scopes` fixture to return `[]`.

### Code style

- Line length: 120
- Ruff rules: `A`, `B`, `C`, `E`, `F`, `I`, `W`, `N`, `C4`, `T20`, `PTH`
- Python 3.13+

## Project layout

```
src/fca_mcp/
├── __main__.py              # python -m fca_mcp → CLI
├── cli.py                   # typer CLI (serve / stdio)
├── settings.py              # Pydantic-Settings config
├── logging.py               # dictConfig-based logging setup
├── uvcorn_app.py            # Starlette/uvicorn HTTP app factory
├── azure/                   # Azure Blob key-value store + client factory
├── http/                    # Interactive OAuth UI routes + static assets
└── server/
    ├── __init__.py          # get_server(): mounts sub-servers + middleware
    ├── app.py               # FcaApp lifespan container
    ├── deps.py              # FastMCP Depends wiring
    ├── search.py            # FRN/IRN/PRN identifier search
    ├── firms.py             # 15 firm tools
    ├── funds.py             # 3 fund tools
    ├── individuals.py       # 3 individual tools
    ├── markets.py           # get_regulated_markets
    ├── auth/                # Auth provider, scopes, tags
    └── types/               # Reflected Pydantic models
```

## License

MIT — see [LICENSE](LICENSE).

## Related

- [fca-api](https://github.com/release-art/fca-api) — the underlying FCA Register REST client.
- [FastMCP](https://gofastmcp.com/) — MCP server framework.
- [FCA Financial Services Register](https://register.fca.org.uk/) — upstream data source.
