A minimal Python agent — about 80 lines — that authenticates against the Candidate MCP, runs the OTP flow, and retrieves a candidate's report through an OpenAI tool-calling loop.
pip install mcp openai httpx python-dotenvmcp— official MCP client (handles protocol, sessions, transport)openai— LLM with function callinghttpx— OAuth token exchangepython-dotenv— load credentials from.env
Create a .env next to your script. Each value has a different source:
# From Checkr Dashboard → Account → Developer Settings → Generate MCP Credentials
OAUTH_CLIENT_ID=<your_client_id>
OAUTH_CLIENT_SECRET=<your_client_secret> # shown once — store in a secrets manager
OAUTH_TOKEN_ENDPOINT=<your_token_endpoint>
# Production: https://mcp.checkr.com/candidate/
# Staging: https://mcp.checkr-staging.com/candidate/
MCP_URL=https://mcp.checkr-staging.com/candidate/
# Generate at https://platform.openai.com/api-keys — swap for any
# function-calling-capable model (Claude, Gemini, etc.)
OPENAI_API_KEY=<your_openai_key>
OPENAI_BASE_URL=https://api.openai.com/v1Load the env vars and instantiate the OpenAI client at module scope.
import asyncio
import json
import os
import httpx
from dotenv import load_dotenv
from mcp.client.session import ClientSession
from mcp.client.streamable_http import streamablehttp_client
from openai import OpenAI
load_dotenv()
llm = OpenAI(
api_key=os.environ["OPENAI_API_KEY"],
base_url=os.environ.get("OPENAI_BASE_URL"),
)
SYSTEM_PROMPT = (
"You are a background check assistant. "
"Use the available tools to verify the candidate's identity "
"and retrieve their report. Present all data clearly. "
"Never fabricate information."
)get_token() runs an OAuth Client Credentials grant against the token endpoint.
def get_token():
resp = httpx.post(os.environ["OAUTH_TOKEN_ENDPOINT"], data={
"grant_type": "client_credentials",
"client_id": os.environ["OAUTH_CLIENT_ID"],
"client_secret": os.environ["OAUTH_CLIENT_SECRET"],
"scope": "candidate:read",
})
resp.raise_for_status()
return resp.json()["access_token"]The MCP server advertises its tools in MCP format; OpenAI's function-calling API expects a slightly different shape. mcp_tools_to_openai() translates between them.
def mcp_tools_to_openai(tools):
return [{
"type": "function",
"function": {
"name": tool.name,
"description": tool.description or "",
"parameters": tool.inputSchema or {"type": "object", "properties": {}},
},
} for tool in tools]run_agent() opens an MCP session, discovers the tools, then loops: read user input → ask the LLM → if it asks for a tool, call MCP and feed the result back → otherwise print the response.
async def run_agent():
async with streamablehttp_client(
url=os.environ["MCP_URL"],
headers={"Authorization": f"Bearer {get_token()}"},
) as (read, write, _):
async with ClientSession(read, write) as session:
await session.initialize()
tools_result = await session.list_tools()
openai_tools = mcp_tools_to_openai(tools_result.tools)
print(f"Connected. Tools: {[t.name for t in tools_result.tools]}")
messages = [{"role": "system", "content": SYSTEM_PROMPT}]
while True:
user_input = input("\nYou: ").strip()
if user_input.lower() in ("quit", "exit"):
break
if not user_input:
continue
messages.append({"role": "user", "content": user_input})
while True:
response = llm.chat.completions.create(
model="gpt-4o",
messages=messages,
tools=openai_tools,
)
choice = response.choices[0]
messages.append(choice.message.model_dump(exclude_none=True))
if choice.finish_reason != "tool_calls":
print(f"\nAgent: {choice.message.content}")
break
for tool_call in choice.message.tool_calls:
tool_name = tool_call.function.name
tool_args = json.loads(tool_call.function.arguments)
print(f" [calling {tool_name}({tool_args})]")
result = await session.call_tool(tool_name, tool_args)
messages.append({
"role": "tool",
"tool_call_id": tool_call.id,
"content": json.dumps(
[b.model_dump() for b in result.content],
default=str,
),
})if __name__ == "__main__":
asyncio.run(run_agent())Save as agent.py:
"""Minimal Checkr Candidate MCP agent."""
import asyncio
import json
import os
import httpx
from dotenv import load_dotenv
from mcp.client.session import ClientSession
from mcp.client.streamable_http import streamablehttp_client
from openai import OpenAI
load_dotenv()
llm = OpenAI(
api_key=os.environ["OPENAI_API_KEY"],
base_url=os.environ.get("OPENAI_BASE_URL"),
)
SYSTEM_PROMPT = (
"You are a background check assistant. "
"Use the available tools to verify the candidate's identity "
"and retrieve their report. Present all data clearly. "
"Never fabricate information."
)
def get_token():
resp = httpx.post(os.environ["OAUTH_TOKEN_ENDPOINT"], data={
"grant_type": "client_credentials",
"client_id": os.environ["OAUTH_CLIENT_ID"],
"client_secret": os.environ["OAUTH_CLIENT_SECRET"],
"scope": "candidate:read",
})
resp.raise_for_status()
return resp.json()["access_token"]
def mcp_tools_to_openai(tools):
return [{
"type": "function",
"function": {
"name": tool.name,
"description": tool.description or "",
"parameters": tool.inputSchema or {"type": "object", "properties": {}},
},
} for tool in tools]
async def run_agent():
async with streamablehttp_client(
url=os.environ["MCP_URL"],
headers={"Authorization": f"Bearer {get_token()}"},
) as (read, write, _):
async with ClientSession(read, write) as session:
await session.initialize()
tools_result = await session.list_tools()
openai_tools = mcp_tools_to_openai(tools_result.tools)
print(f"Connected. Tools: {[t.name for t in tools_result.tools]}")
messages = [{"role": "system", "content": SYSTEM_PROMPT}]
while True:
user_input = input("\nYou: ").strip()
if user_input.lower() in ("quit", "exit"):
break
if not user_input:
continue
messages.append({"role": "user", "content": user_input})
while True:
response = llm.chat.completions.create(
model="gpt-4o",
messages=messages,
tools=openai_tools,
)
choice = response.choices[0]
messages.append(choice.message.model_dump(exclude_none=True))
if choice.finish_reason != "tool_calls":
print(f"\nAgent: {choice.message.content}")
break
for tool_call in choice.message.tool_calls:
tool_name = tool_call.function.name
tool_args = json.loads(tool_call.function.arguments)
print(f" [calling {tool_name}({tool_args})]")
result = await session.call_tool(tool_name, tool_args)
messages.append({
"role": "tool",
"tool_call_id": tool_call.id,
"content": json.dumps(
[b.model_dump() for b in result.content],
default=str,
),
})
if __name__ == "__main__":
asyncio.run(run_agent())python agent.pyConnected. Tools: ['request_candidate_verification', 'verify_candidate_otp', 'get_report']
You: I need to check my background report
Agent: I can help with that. What email address is associated with your background check?
You: alice@example.com
[calling request_candidate_verification({"email": "alice@example.com"})]
Agent: A verification code has been sent to your email. Please enter the 6-digit code.
You: 482910
[calling verify_candidate_otp({"email": "alice@example.com", "code": "482910"})]
[calling get_report()]
Agent: Here are your background check results...- Swap the LLM — replace
OpenAI()with any SDK that supports function calling. Themcp_tools_to_openaiconverter would change shape; the rest of the loop is the same. - Cache the token — bearer tokens last 1 hour. In production, cache and refresh shortly before expiry rather than minting a fresh token per request.
- Add a web UI — wrap
run_agent()in a FastAPI/Flask endpoint instead of terminal input. - Node — use the JS/TypeScript
@modelcontextprotocol/sdkwith the sameClient+StreamableHTTPClientTransportpattern.