MCP Client Guide
The MCPClient is the primary interface for agents to call tools through Agentic Fabriq. It handles authentication, token exchange, and JSON-RPC communication with the MCP server.
Table of Contents
Prerequisites
Before using the MCP client, ensure you have completed the following setup:
1. Register an Application
Applications act as the identity for your agent. Registration creates the credentials that allow your agent to access tools on behalf of users.
# Register your application (requires authentication)
afctl auth login
afctl applications register \
--app-id my-email-agent \
--scopes google:gmail.send,google:gmail.readonly,slack:chat:write \
--display-name "My Email Agent"
# Optional: provide IdP credentials for SSO/token exchange
afctl applications register \
--app-id my-email-agent \
--scopes google:gmail.send,google:gmail.readonly,slack:chat:write \
--display-name "My Email Agent" \
--idp-client-id <idp-client-id> \
--idp-client-secret <idp-client-secret>If you do not pass the IdP flags, afctl prompts you for IdP credentials during registration. Press Enter to skip them if you are not using SSO, or pass --skip-idp to skip the prompt entirely.
After registration, you'll have:
app_id: Your application identifier (e.g.,org-abc123_my-email-agent)app_secret: Your application client secret (e.g.,sk_live_xyz789...)
These credentials are saved to ~/.af/applications/{app_id}.json.
2. User Authentication
Users must authenticate before the MCP client can access their tools. The authentication method depends on your deployment:
| Scenario | Auth Method | How User Authenticates |
|---|---|---|
| Local development/testing | cli | User runs afctl auth login |
| Web app with Keycloak | keycloak | User logs in via Keycloak, app receives JWT |
| Enterprise SSO (Okta) | idp | User logs in via Okta, app receives Okta JWT |
3. Tool Connections
Users must connect the tools they want to use. This is done via:
- Dashboard UI: Users connect tools in the Agentic Fabriq dashboard
- CLI:
afctl tools addandafctl tools connect
The MCP client can only access tools that:
- The user has connected
- The application has scopes for
Installation
pip install agentic-fabriq-sdkImport the client:
from af_sdk import MCPClientAuthentication Methods
The MCP client supports three authentication methods, each suited for different deployment scenarios.
CLI Method (method="cli")
Use case: Local development, testing, CLI scripts, or any scenario where the user has already authenticated via afctl auth login.
How it works: The client reads stored credentials from ~/.af/credentials.json (created by afctl auth login) and exchanges them for an AF token.
Required parameters: app_id, app_secret
# User must first run: afctl auth login
async with MCPClient(
method="cli",
app_id="org-abc123_my-agent",
app_secret="sk_live_xyz789..."
) as client:
tools = await client.list_tools()Keycloak Method (method="keycloak")
Use case: Web applications or services that authenticate users directly through Keycloak (Agentic Fabriq's identity provider).
Required parameters: app_id, app_secret, keycloak_token
# Token obtained from your Keycloak OAuth flow
keycloak_token = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."
async with MCPClient(
method="keycloak",
app_id="org-abc123_my-agent",
app_secret="sk_live_xyz789...",
keycloak_token=keycloak_token
) as client:
result = await client.call_tool("slack_post_message", {
"channel": "#general",
"text": "Hello from my web app!"
})IdP Method (method="idp")
Use case: Enterprise organizations using external identity providers like Okta for SSO.
Required parameters: app_id, app_secret, external_token
Optional parameters: org_url (auto-detected from app if not provided, used to identify organization that user is in)
# Token obtained from Okta OAuth flow
okta_token = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."
async with MCPClient(
method="idp",
app_id="org-abc123_my-agent",
app_secret="sk_live_xyz789...",
external_token=okta_token,
org_url="mycompany.com" # Optional
) as client:
result = await client.call_tool("notion_search", {
"query": "Q4 planning"
})Basic Usage Patterns
Async Context Manager (Recommended)
The async context manager automatically handles connection and cleanup:
from af_sdk import MCPClient
async with MCPClient(
method="cli",
app_id="org-abc123_my-agent",
app_secret="sk_live_xyz789..."
) as client:
# Client is connected and authenticated
tools = await client.list_tools()
result = await client.call_tool("google_gmail_list_messages", {"max_results": 10})
# Connection automatically closed on exitSync Context Manager
For synchronous code, use the sync context manager with _sync method variants:
from af_sdk import MCPClient
with MCPClient(
method="cli",
app_id="org-abc123_my-agent",
app_secret="sk_live_xyz789..."
) as client:
tools = client.list_tools_sync()
result = client.call_tool_sync("slack_post_message", {
"channel": "#general",
"text": "Hello from sync code!"
})Manual Connection Management
For fine-grained control over the connection lifecycle:
from af_sdk import MCPClient
client = MCPClient(
method="cli",
app_id="org-abc123_my-agent",
app_secret="sk_live_xyz789..."
)
try:
# Explicitly connect
await client.connect()
# Use the client
tools = await client.list_tools()
result = await client.call_tool("slack_post_message", {
"channel": "#test",
"text": "Hello!"
})
finally:
# Always disconnect
await client.disconnect()Working with Tools
Listing Available Tools
The list_tools() method fetches all tools available to the current user based on their connections and the application's scopes:
async with MCPClient(method="cli", app_id="...", app_secret="...") as client:
tools = await client.list_tools()
print(f"Found {len(tools)} tools")
for tool in tools:
print(f"\nTool: {tool['name']}")
print(f" Description: {tool['description']}")
print(f" Parameters: {tool['inputSchema']}")Each tool definition includes:
name: The tool identifier (e.g.,google_gmail_send_email)description: Human-readable description of what the tool doesinputSchema: JSON Schema defining the tool's parameters
Accessing Cached Tools
After calling list_tools(), tools are cached for quick access:
async with MCPClient(method="cli", app_id="...", app_secret="...") as client:
await client.list_tools() # Populates cache
# Quick access to tool names
print(client.tool_names)
# ['google_gmail_list_messages', 'google_gmail_send_email', 'slack_post_message', ...]
# Check if a specific tool is available
if client.has_tool("google_gmail_send_email"):
print("Gmail send is available!")
# Get full tool definition
tool = client.get_tool("slack_post_message")
if tool:
print(f"Slack tool schema: {tool['inputSchema']}")
# Get all cached tools (no network call)
all_tools = client.get_tools()Understanding Tool Schemas
Each tool has an inputSchema that defines its parameters:
tool = client.get_tool("google_gmail_send_email")
print(tool['inputSchema'])Example schema:
{
"type": "object",
"properties": {
"to": {
"type": "string",
"description": "Recipient email address"
},
"subject": {
"type": "string",
"description": "Email subject line"
},
"body": {
"type": "string",
"description": "Email body content"
},
"cc": {
"type": "string",
"description": "CC recipients (optional)"
}
},
"required": ["to", "subject", "body"]
}Calling Tools
Use call_tool() with the tool name and a dictionary of arguments:
result = await client.call_tool("tool_name", {
"param1": "value1",
"param2": "value2"
})The result format depends on the specific tool. Most tools return dictionaries or lists.
Configuration Options
| Parameter | Type | Default | Description |
|---|---|---|---|
method | str | required | Authentication method: "cli", "keycloak", or "idp" |
app_id | str | required | Application ID from afctl applications register |
app_secret | str | required | Application client secret from afctl applications register |
keycloak_token | str | None | Keycloak JWT token. Required when method="keycloak" |
external_token | str | None | External IdP token (e.g., Okta). Required when method="idp" |
org_url | str | None | Organization URL (e.g., "mycompany.com"). Used with method="idp" to determine the correct Keycloak realm. |
mcp_url | str | https://dashboard.agenticfabriq.com/mcp | MCP server URL. Override for self-hosted or staging environments. |
gateway_url | str | https://dashboard.agenticfabriq.com | Gateway URL for token exchange. Override for self-hosted or staging environments. |
timeout | float | 60.0 | Request timeout in seconds for MCP calls. |
Properties and Methods
Properties
| Property | Type | Description |
|---|---|---|
is_connected | bool | True if the client is connected and authenticated |
tool_names | List[str] | List of available tool names (from cache) |
token_info | AFTokenResponse | Token metadata including user_id, app_id, expires_in, token_type |
Connection Methods
| Method | Returns | Description |
|---|---|---|
connect() | None | Authenticate and connect to the MCP server. Called automatically by context manager. |
disconnect() | None | Close the connection and clear state. Called automatically by context manager. |
connect_sync() | None | Synchronous version of connect() |
disconnect_sync() | None | Synchronous version of disconnect() |
Tool Methods
| Method | Returns | Description |
|---|---|---|
list_tools() | List[Dict] | Fetch available tools from the server. Updates the internal cache. |
call_tool(name, arguments) | Any | Execute a tool with the given arguments. Returns the tool's result. |
get_tools() | List[Dict] | Get the cached tool list (no network call). |
get_tool(name) | Dict | None | Get a specific tool definition from the cache by name. |
has_tool(name) | bool | Check if a tool exists in the cache. |
list_tools_sync() | List[Dict] | Synchronous version of list_tools() |
call_tool_sync(name, arguments) | Any | Synchronous version of call_tool() |
Error Handling
The SDK provides specific exception types for different error scenarios:
from af_sdk import MCPClient
from af_sdk.exceptions import (
AuthenticationError,
MCPConnectionError,
MCPError
)
try:
async with MCPClient(
method="cli",
app_id="org-abc123_my-agent",
app_secret="sk_live_xyz789..."
) as client:
result = await client.call_tool("google_gmail_send_email", {
"to": "user@example.com",
"subject": "Test",
"body": "Hello"
})
except AuthenticationError as e:
# Authentication failed
# Causes: Token expired, invalid credentials, not logged in, invalid app_id/secret
print(f"Authentication failed: {e}")
print("Suggestion: Run 'afctl auth login' or check your app credentials")
except MCPConnectionError as e:
# Connection failed
# Causes: Network issues, server unreachable, timeout, connection not established
print(f"Connection failed: {e}")
print("Suggestion: Check network connectivity and MCP server URL")
except MCPError as e:
# MCP server returned an error
# Causes: Invalid tool name, bad parameters, tool execution failure, permission denied
print(f"MCP error: {e.message}")
print(f"Error code: {e.code}")
print(f"Additional details: {e.data}")Exception Reference
| Exception | Causes | Resolution |
|---|---|---|
AuthenticationError | Token expired, invalid/missing credentials, not logged in, invalid app_id or app_secret | Run afctl auth login, verify app credentials |
MCPConnectionError | Network failure, server unreachable, DNS issues, firewall blocking, calling methods without connecting first | Check network, verify mcp_url, ensure connect() was called |
MCPError | Tool not found, invalid parameters, tool execution failed, missing tool connection, insufficient scopes | Verify tool name, check parameter schema, ensure user has connected the tool |
Advanced Usage
Custom Server URLs
For self-hosted deployments or staging environments:
async with MCPClient(
method="cli",
app_id="org-abc123_my-agent",
app_secret="sk_live_xyz789...",
mcp_url="https://mcp.mycompany.com/mcp",
gateway_url="https://api.mycompany.com"
) as client:
tools = await client.list_tools()Inspecting Token Information
After connecting, you can inspect the authenticated token:
async with MCPClient(method="cli", app_id="...", app_secret="...") as client:
# Token info is available after connect
token_info = client.token_info
print(f"User ID: {token_info.user_id}")
print(f"App ID: {token_info.app_id}")
print(f"Expires in: {token_info.expires_in} seconds")
print(f"Token type: {token_info.token_type}")Reusing Connections
For applications that make many tool calls, keep the connection open:
class MyAgent:
def __init__(self, app_id: str, app_secret: str):
self.client = MCPClient(
method="cli",
app_id=app_id,
app_secret=app_secret
)
async def start(self):
await self.client.connect()
await self.client.list_tools()
async def stop(self):
await self.client.disconnect()
async def send_email(self, to: str, subject: str, body: str):
return await self.client.call_tool("google_gmail_send_email", {
"to": to,
"subject": subject,
"body": body
})
async def post_slack(self, channel: str, text: str):
return await self.client.call_tool("slack_post_message", {
"channel": channel,
"text": text
})
# Usage
agent = MyAgent("org-abc123_my-agent", "sk_live_xyz789...")
await agent.start()
try:
await agent.send_email("user@example.com", "Hello", "World")
await agent.post_slack("#general", "Email sent!")
finally:
await agent.stop()Custom Timeout
For long-running tool operations:
async with MCPClient(
method="cli",
app_id="org-abc123_my-agent",
app_secret="sk_live_xyz789...",
timeout=120.0 # 2 minute timeout
) as client:
# Long-running operation
result = await client.call_tool("some_slow_tool", {"large_data": "..."})Best Practices
1. Always Use Context Managers
Context managers ensure proper cleanup even if errors occur:
# Good
async with MCPClient(...) as client:
await client.call_tool(...)
# Avoid (requires manual cleanup)
client = MCPClient(...)
await client.connect()
# If an error occurs here, disconnect() may not be called
await client.call_tool(...)
await client.disconnect()2. Check Tool Availability Before Calling
async with MCPClient(...) as client:
await client.list_tools()
if not client.has_tool("google_gmail_send_email"):
print("Gmail not available - user may need to connect it")
return
await client.call_tool("google_gmail_send_email", {...})3. Handle Errors Gracefully
from af_sdk.exceptions import AuthenticationError, MCPError
async def send_notification(client, message):
try:
await client.call_tool("slack_post_message", {
"channel": "#alerts",
"text": message
})
return True
except MCPError as e:
logger.error(f"Failed to send Slack notification: {e.message}")
return False4. Cache Tool Lists When Possible
If your application makes many calls, avoid repeated list_tools() calls:
async with MCPClient(...) as client:
# Fetch once
await client.list_tools()
# Use cached data for subsequent checks
if client.has_tool("google_gmail_send_email"):
await client.call_tool("google_gmail_send_email", {...})
if client.has_tool("slack_post_message"):
await client.call_tool("slack_post_message", {...})5. Use Appropriate Auth Method
| Scenario | Recommended Method |
|---|---|
| Local development | cli |
| CI/CD pipelines | cli with service account |
| Web applications (standard) | keycloak |
| Enterprise apps with Okta | idp |
Complete Examples
Email Assistant Agent
import asyncio
from af_sdk import MCPClient
from af_sdk.exceptions import AuthenticationError, MCPError
async def email_assistant():
"""An agent that summarizes unread emails and posts to Slack."""
try:
async with MCPClient(
method="cli",
app_id="org-abc123_email-assistant",
app_secret="sk_live_xyz789..."
) as client:
# Check what tools are available
await client.list_tools()
has_gmail = client.has_tool("google_gmail_list_messages")
has_slack = client.has_tool("slack_post_message")
if not has_gmail:
print("Error: Gmail not connected")
return
# Fetch unread emails
messages = await client.call_tool("google_gmail_list_messages", {
"max_results": 10,
"query": "is:unread"
})
if not messages:
print("No unread emails")
return
# Build summary
summary = f"📧 You have {len(messages)} unread emails:\n"
for msg in messages[:5]:
subject = msg.get("subject", "No subject")
sender = msg.get("from", "Unknown")
summary += f"• {subject} (from {sender})\n"
if len(messages) > 5:
summary += f"... and {len(messages) - 5} more"
print(summary)
# Post to Slack if available
if has_slack:
await client.call_tool("slack_post_message", {
"channel": "#daily-digest",
"text": summary
})
print("Summary posted to Slack!")
except AuthenticationError as e:
print(f"Authentication failed: {e}")
print("Run: afctl auth login")
except MCPError as e:
print(f"Tool error: {e.message}")
if __name__ == "__main__":
asyncio.run(email_assistant())Web App with Keycloak
from fastapi import FastAPI, Depends, HTTPException
from af_sdk import MCPClient
from af_sdk.exceptions import AuthenticationError, MCPError
app = FastAPI()
APP_ID = "org-abc123_web-app"
APP_SECRET = "sk_live_xyz789..."
async def get_mcp_client(keycloak_token: str):
"""Create an MCP client for the authenticated user."""
client = MCPClient(
method="keycloak",
app_id=APP_ID,
app_secret=APP_SECRET,
keycloak_token=keycloak_token
)
await client.connect()
return client
@app.post("/api/send-email")
async def send_email(
to: str,
subject: str,
body: str,
token: str = Depends(get_keycloak_token) # Your auth dependency
):
try:
client = await get_mcp_client(token)
try:
result = await client.call_tool("google_gmail_send_email", {
"to": to,
"subject": subject,
"body": body
})
return {"success": True, "result": result}
finally:
await client.disconnect()
except AuthenticationError:
raise HTTPException(401, "Authentication failed")
except MCPError as e:
raise HTTPException(500, f"Tool error: {e.message}")
@app.get("/api/tools")
async def list_tools(token: str = Depends(get_keycloak_token)):
try:
client = await get_mcp_client(token)
try:
tools = await client.list_tools()
return {"tools": client.tool_names}
finally:
await client.disconnect()
except AuthenticationError:
raise HTTPException(401, "Authentication failed")Synchronous Script
from af_sdk import MCPClient
from af_sdk.exceptions import MCPError
def daily_report():
"""Sync script that generates a daily report."""
with MCPClient(
method="cli",
app_id="org-abc123_reporter",
app_secret="sk_live_xyz789..."
) as client:
# List tools (sync)
client.list_tools_sync()
# Get calendar events
if client.has_tool("google_calendar_list_events"):
events = client.call_tool_sync("google_calendar_list_events", {
"time_min": "2024-01-15T00:00:00Z",
"time_max": "2024-01-15T23:59:59Z"
})
print(f"Today's events: {len(events)}")
# Get unread emails count
if client.has_tool("google_gmail_list_messages"):
messages = client.call_tool_sync("google_gmail_list_messages", {
"max_results": 100,
"query": "is:unread"
})
print(f"Unread emails: {len(messages)}")
if __name__ == "__main__":
daily_report()Need help?
Our team is here to help you get started.