Skip to content
Last updated

Building Your Own Agent

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.

Dependencies

pip install mcp openai httpx python-dotenv
  • mcp — official MCP client (handles protocol, sessions, transport)
  • openai — LLM with function calling
  • httpx — OAuth token exchange
  • python-dotenv — load credentials from .env

Configuration

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/v1

Imports and client setup

Load 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 a bearer token

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"]

Adapt MCP tools to OpenAI's format

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 the agent loop

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,
                            ),
                        })

Entry point

if __name__ == "__main__":
    asyncio.run(run_agent())

Full file

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())

Run it

python agent.py
Connected. 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...

Adapting to your stack

  • Swap the LLM — replace OpenAI() with any SDK that supports function calling. The mcp_tools_to_openai converter 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/sdk with the same Client + StreamableHTTPClientTransport pattern.