Python client library for connecting to the Civic MCP Hub with direct bearer token auth or RFC 8693 token exchange.
uv syncInstall optional integration extras:
uv sync --extra pydanticai --extra langchain --extra fastmcpfrom civic_mcp_client import CivicMCPClient
access_token = "your-civic-access-token" # e.g., from your session or the demo app
client = CivicMCPClient(
auth={"token": access_token},
)
tools = await client.get_tools()
instructions = await client.get_server_instructions()
result = await client.call_tool(name="tool-name", args={"foo": "bar"})
await client.close()Recommended: use the demo app for a production-like frontend + Python backend setup.
Start with the reference implementation here:
That demo uses @civic/auth in the frontend and forwards the authenticated Civic access token to the Python backend (which then passes it into CivicMCPClient(auth={"token": ...})).
Use the existing Civic install flow:
- Open https://app.civic.com/web/install/mcp-url
- Click Generate token
- Copy the returned
access_token - Set it in your app as
CIVIC_ACCESS_TOKEN
Treat this as a secret and keep it out of source control. Token expiry and rotation follow Civic's token lifecycle.
If your frontend is separate from your Python backend (for example, a Next.js frontend with @civic/auth/nextjs):
- Frontend obtains the user session token at runtime
- Backend receives that token and passes it to
CivicMCPClient(auth={"token": ...}) - Keep
CIVIC_CLIENT_IDandCIVIC_PROFILE_IDas app configuration values
The manual self-serve flow above is useful for local testing when you want to bypass a full frontend session.
import os
from civic_mcp_client import CivicMCPClient
client = CivicMCPClient(
auth={
"token_exchange": {
"client_id": os.environ["CIVIC_CLIENT_ID"],
"client_secret": os.environ["CIVIC_CLIENT_SECRET"],
"subject_token": lambda: "external-token",
"expires_in": 3600, # optional requested lifetime in seconds
}
},
)The token exchange manager provides:
- cache + expiry handling
- token change detection
- in-flight deduplication for concurrent calls
- expiry buffer of
min(30s, expires_in / 2)
Current access token can be retrieved via:
token = await client.get_access_token()uv run --extra test python -m pytest
uv run --extra test python -m pytest -m integrationRunnable scripts are available in examples/.
uv run python examples/direct_token.py
uv run python examples/token_exchange.py
uv run python examples/langchain_adapter.py
uv run python examples/pydanticai_adapter.py
uv run python examples/fastmcp_backend.pyTo load environment variables from a .env file:
uv sync --extra examples
cp .env.example .env
uv run python -m dotenv run -- python examples/direct_token.pyOr, if you prefer running from inside the examples/ directory:
cd examples
cp ../.env.example .env
uv run python -m dotenv run -- python direct_token.pyUse await client.adapt_for(...) with the adapter for your framework. It returns either a new CivicMCPClient (for backend adapters like FastMCP) or adapter-native tool output (e.g. list of schemas or tool definitions).
from civic_mcp_client import CivicMCPClient
from civic_mcp_client.adapters.pydanticai import pydanticai
client = CivicMCPClient(auth={"token": "your-civic-access-token"})
tools = await client.adapt_for(pydanticai())from civic_mcp_client import CivicMCPClient
from civic_mcp_client.adapters.langchain import execute_langchain_tool_call, langchain
client = CivicMCPClient(auth={"token": "your-civic-access-token"})
tool_schemas = await client.adapt_for(langchain())
# model = model.bind_tools(tool_schemas)
# response = model.invoke("...")
# tool_result = await execute_langchain_tool_call(client, response.tool_calls[0])from civic_mcp_client import CivicMCPClient
from civic_mcp_client.adapters.fastmcp import fastmcp
client = CivicMCPClient(
auth={"token": "your-civic-access-token"},
)
fastmcp_client = await client.adapt_for(fastmcp())
# fastmcp_client is a CivicMCPClient with FastMCP backend; auth/headers come from config
tools = await fastmcp_client.get_tools()If you want to lock requests to a specific profile, pass civic_profile:
client = CivicMCPClient(
auth={"token": "your-civic-access-token"},
civic_profile="optional-profile-id",
)civic_accountwas removed from Python client config to match the updated TypeScript direction.civic_profileremains supported and maps tox-civic-profile-id.TokenExchangeConfigsupportsexpires_inandlock_to_profile(account lock removed).