Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
f9ce52a
#43: Enhanced `find_schemas` and `find_tables`
ahsimb Aug 24, 2025
f21c538
Merge remote-tracking branch 'origin/main'
ahsimb Aug 26, 2025
9de5e34
Merge remote-tracking branch 'origin/main'
ahsimb Aug 28, 2025
0903dd1
Merge remote-tracking branch 'origin/main'
ahsimb Aug 28, 2025
cc3b483
Merge remote-tracking branch 'origin/main'
ahsimb Aug 29, 2025
ca66bcc
Merge remote-tracking branch 'origin/main'
ahsimb Aug 29, 2025
c68781c
Merge remote-tracking branch 'origin/main'
ahsimb Aug 29, 2025
a09aca7
Merge remote-tracking branch 'origin/main'
ahsimb Sep 3, 2025
421d9c1
Merge remote-tracking branch 'origin/main'
ahsimb Sep 4, 2025
42c5317
Merge remote-tracking branch 'origin/main'
ahsimb Sep 4, 2025
d75ce42
Merge remote-tracking branch 'origin/main'
ahsimb Sep 4, 2025
dd4f856
Merge remote-tracking branch 'origin/main'
ahsimb Sep 4, 2025
cfcf56b
Merge remote-tracking branch 'origin/main'
ahsimb Sep 4, 2025
e8b82ad
Merge remote-tracking branch 'origin/main'
ahsimb Oct 16, 2025
f2b7115
Merge remote-tracking branch 'origin/main'
ahsimb Oct 17, 2025
946f7bc
#70: Started the User Guide
ahsimb Oct 20, 2025
b638baa
#70: doc checkpoint 1
ahsimb Oct 23, 2025
00446b8
#70: checkpoint 2
ahsimb Oct 24, 2025
f6c0b1f
Merge remote-tracking branch 'origin/main' into documentation/70-user…
ahsimb Oct 24, 2025
e5c98bb
#70: finished the guide
ahsimb Oct 28, 2025
6b24442
#70: removed doc build
ahsimb Oct 29, 2025
a89d74d
#70: removed doc build
ahsimb Oct 29, 2025
204202d
kick off CI
ahsimb Oct 29, 2025
cf1193d
#70: removed doc build
ahsimb Oct 29, 2025
60c49ae
#70: small change in db conn user guide
ahsimb Oct 29, 2025
e9df820
#70: addressed review comments
ahsimb Oct 29, 2025
6b38ca1
Merge remote-tracking branch 'origin/main'
ahsimb Oct 29, 2025
e677ce8
Merge remote-tracking branch 'origin/main'
ahsimb Oct 29, 2025
a64bc87
Merge remote-tracking branch 'origin/main'
ahsimb Oct 29, 2025
96c42f1
Merge remote-tracking branch 'origin/main'
ahsimb Nov 10, 2025
8fa4003
Merge remote-tracking branch 'origin/main'
ahsimb Nov 12, 2025
8dbc731
Merge remote-tracking branch 'origin/main'
ahsimb Nov 12, 2025
b00bc4c
#90: added manual option
ahsimb Nov 13, 2025
36564e0
Merge remote-tracking branch 'origin/main' into feature/92-introspect…
ahsimb Nov 13, 2025
b9c3efd
#90: moved to ITDE 4.4.1
ahsimb Nov 13, 2025
1e5991e
#90: moved to FastMCP 2.13
ahsimb Nov 13, 2025
a00d52c
#92: added COMMIT to the db_setup
ahsimb Nov 13, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions doc/changes/unreleased.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
* #84: Added support for SaaS backend.
* #87: Added support for DML and DDL queries.
* #86: Added TLS/SSL parameters to the connection factory.
* #92: Added support for OAuth introspection tokens.

## Refactoring

Expand Down
19 changes: 16 additions & 3 deletions doc/user_guide/open_id_setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,8 +100,10 @@ Both `Remote OAuth` and `OAuthProxy` require a [Token Verifier](https://gofastmc
The choice of the token verifier depends on the token verification model supported by a
particular identity provider.

At the time of writing, the latest release of the FastMCP - 2.12.5 - only offers the
JWT Token Verification. The Opaque Token Verification should be added soon.
At the time of writing, the latest release of the FastMCP - 2.13.0 - offers two types of
verification - [JWT Token Verification](https://gofastmcp.com/servers/auth/token-verification#jwt-token-verification)
and [Opaque Token Verification](https://gofastmcp.com/servers/auth/token-verification#opaque-token-verification).
The variation of the former is [Static Public Key Verification](https://gofastmcp.com/servers/auth/token-verification#static-public-key-verification)

A token verifier provider may be the only module needed if the MCP Authentication uses
an externally supplied bearer token. However, this guide doesn't give any details on how
Expand All @@ -118,10 +120,21 @@ such a system could be configured. Also, in this scenario, we recommend using Fa
| EXA_AUTH_AUDIENCE | no | Expected audience claim(s). |
| EXA_AUTH_ALGORITHM | no | JWT signing algorithm. Supported algorithms: <br/>- Asymmetric: RS256/384/512, ES256/384/512, PS256/384/512 (default: RS256). <br/>- Symmetric: HS256, HS384, HS512. |
| EXA_AUTH_REQUIRED_SCOPES | no | Required scopes for all tokens. |
| EXA_AUTH_BASE_URL | no | Base URL for TokenVerifier protocol. |
| EXA_AUTH_BASE_URL | yes | Base URL for TokenVerifier protocol. |

Either of `EXA_AUTH_PUBLIC_KEY` or `EXA_AUTH_JWKS_URI` must be set.

#### Opaque Token Verification (Token Introspection)

| Variable Name | Required | Value |
|----------------------------|:--------:|-----------------------------------------------------------------------|
| EXA_AUTH_INTROSPECTION_URL | yes | URL of the OAuth token introspection endpoint. |
| EXA_AUTH_CLIENT_ID | yes | OAuth client ID for authenticating to the introspection endpoint. |
| EXA_AUTH_CLIENT_SECRET | yes | OAuth client secret for authenticating to the introspection endpoint. |
| EXA_AUTH_TIMEOUT_SECONDS | no | HTTP request timeout in seconds (default: 10). |
| EXA_AUTH_REQUIRED_SCOPES | no | Required scopes for all tokens. |
| EXA_AUTH_BASE_URL | yes | Base URL for TokenVerifier protocol. |

### Remote OAuth

| Variable Name | Required | Value |
Expand Down
87 changes: 79 additions & 8 deletions exasol/ai/mcp/server/generic_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import os
from collections.abc import Callable
from dataclasses import dataclass
from functools import cache
from io import StringIO
from typing import Any

Expand All @@ -33,6 +34,8 @@
OAuthProxy,
RemoteAuthProvider,
)
from fastmcp.server.auth.auth import TokenVerifier
from fastmcp.server.auth.providers.introspection import IntrospectionTokenVerifier
from fastmcp.server.auth.providers.jwt import JWTVerifier

ENV_PROVIDER_TYPE = "FASTMCP_SERVER_AUTH"
Expand Down Expand Up @@ -61,6 +64,10 @@ def str_to_bool(s) -> bool:
raise ValueError(f"Invalid boolean parameter: {s}")


def str_to_int(s: str) -> int:
return int(str_to_str(s))


def str_to_dict(s) -> dict[str, str]:
fields = str_to_list(s)
if (len(fields) % 2) != 0:
Expand Down Expand Up @@ -96,6 +103,17 @@ class AuthProviderInfo:
AuthParameter("base_url"),
],
),
AuthProviderInfo(
provider_type=IntrospectionTokenVerifier,
parameters=[
AuthParameter("introspection_url"),
AuthParameter("client_id"),
AuthParameter("client_secret"),
AuthParameter("timeout_seconds", str_to_int),
AuthParameter("required_scopes", str_to_list),
AuthParameter("base_url"),
],
),
AuthProviderInfo(
provider_type=RemoteAuthProvider,
parameters=[
Expand Down Expand Up @@ -140,6 +158,29 @@ def exa_parameter_env_name(param: AuthParameter) -> str:
return f"EXA_AUTH_{param.name.upper()}"


@cache
def _get_provider_map() -> dict[str, AuthProviderInfo]:
"""
Indexes all known providers by their names as they would apper in the
FASTMCP_SERVER_AUTH envar.
"""
return {
exa_provider_name(provider.provider_type): provider
for provider in _generic_providers
}


@cache
def _get_verifier_map() -> dict[str, AuthProviderInfo]:
"""
Indexes all known Token Verifiers by their names.
"""
return {
exa_provider_name(ver_type): ver_type
for ver_type in [JWTVerifier, IntrospectionTokenVerifier]
}


def create_auth_provider(
provider_info: AuthProviderInfo, **extra_kwargs
) -> AuthProvider:
Expand All @@ -151,24 +192,54 @@ def create_auth_provider(
return provider_info.provider_type(**kwargs, **extra_kwargs)


def get_token_verifier(provider_name: str) -> tuple[TokenVerifier, str]:
"""
Creates one of the known types of a Token Verifier. This can be either
the requested Auth Provider itself, or a part of the requested Auth Provider.
In the latter case, the function will create whatever Verifier it can create
with the information given in the environment variables. Will raise ValueError
if it can create none.
Args:
provider_name: requested Auth Provider.
"""

# First check if the requested Auth provider is one of the Token Verifier types:
provider_map = _get_provider_map()
verifier_map = _get_verifier_map()
provider_type = verifier_map.get(provider_name)
if provider_type is not None:
provider = create_auth_provider(provider_map[provider_name])
return provider, provider_name

# If the requested provider is not a Token Verifier than create a Token Verifier
# that sufficient parameters are provided for.
for ver_name, ver_type in verifier_map.items():
try:
verifier = create_auth_provider(provider_map[ver_name])
return verifier, ver_name
except ValueError:
pass
raise ValueError(
"Insufficient parameters to create any of the supported "
f"Token Verifier types: {verifier_map.keys()}"
)


def get_auth_provider() -> AuthProvider | None:
"""
Creates one of FastMCP generic OAuth2 providers, if the correspondent type name is
set in the FASTMCP_SERVER_AUTH environment variable.
set in the FASTMCP_SERVER_AUTH environment variable. Will raise ValueError if the
requested provider type is unknown or no Token Verifier can be created.
"""
provider_name = os.environ.get(ENV_PROVIDER_TYPE)
if not provider_name:
return None

provider_map = {
exa_provider_name(provider.provider_type): provider
for provider in _generic_providers
}
provider_map = _get_provider_map()
if provider_name not in provider_map:
return None
raise ValueError(f"Generic OAuth provider {provider_name} is not supported ")

verifier_name = exa_provider_name(JWTVerifier)
verifier = create_auth_provider(provider_map[verifier_name])
verifier, verifier_name = get_token_verifier(provider_name)
if provider_name == verifier_name:
return verifier
return create_auth_provider(provider_map[provider_name], token_verifier=verifier)
Expand Down
10 changes: 5 additions & 5 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ repository = "https://github.com/exasol/mcp-server"
homepage = "https://github.com/exasol/mcp-server"

[tool.poetry.dependencies]
fastmcp = "^2.12.5"
fastmcp = "^2.13.0"
pyexasol = "^1.0.0"
sqlglot = "^27.2.0"
numpy = ">=2,<=2.2.0"
Expand All @@ -39,7 +39,7 @@ exasol-saas-api = "^2.4.0"
exasol-toolbox = "^1.6.1"
pytest-exasol-backend = "^1.2.1"
pytest-exasol-extension = "^0.2.4"
exasol-integration-test-docker-environment = "4.3.0"
exasol-integration-test-docker-environment = "^4.4.1"
oidc-provider-mock = "^0.2.9"
flask-oidc = "^2.4.0"

Expand Down
1 change: 1 addition & 0 deletions test/integration/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,7 @@ def setup_database(
)
pyexasol_connection.execute(query=query)

pyexasol_connection.execute(query="COMMIT")
yield

finally:
Expand Down
Loading
Loading