LinkedIn profiles in Agno: a LinkedIn tool through MCP or a Python function
You're building an agent in Python with Agno, formerly Phidata, and the model should read a LinkedIn profile from its URL: the job title, company, location and headline. Say it answers a question about someone's current role, or keeps the job title in a record up to date. Agno has no LinkedIn toolkit of its own. We searched its GitHub repository on October 11, 2026: we found LinkedIn in two toolkits for other vendors' APIs, and in examples.
Datacircle is a data co-op. Step 1: Query your favorite B2B data APIs through us. Same request, same price, no markup. Step 2: You're DONE. Every morning, you get the flat file of your data plus everyone else's. Add $50 to your account: you get $50 of API PLUS the flat file. Right now we have 3 live LinkedIn profile APIs that we trust: Up2Data, HarvestAPI and Fetchin.
You can load our MCP server into your agent and write no tool code. Or you can write one Python function that calls our API, and decide what the model reads. Both call Up2Data first. Up2Data costs $2.375 per 1,000 profiles it finds, and nothing for a profile it can't find. Fetchin is cheaper at $1.485 per 1,000, but it charges for a profile it can't find, and all our customers share its rate limit. Both switch to Fetchin only when Up2Data returns a 429, at its daily limit or its rate limit.
We ran both agents inside Agno, with a scripted stand-in for OpenAI's API as the model, and a stand-in server that answers like our API. We called api.datacircle.dev with a wrong key, from the function and from the MCP code: each got a 401, at no charge. We haven't run either with a real key or a real model.
LinkedIn in Agno's toolkits
Agno's toolkits search and scrape the web and call other vendors' APIs, with your key for each one. Two of them reach LinkedIn profiles, and Agno's docs show a third way:
BrightDataToolshasweb_with the sourcedata_feed linkedin_. It calls Bright Data's API for the profile, with yourperson_ profile BRIGHT_, and Bright Data bills you. Our comparison with Bright Data: Bright Data LinkedIn scraper alternative.DATA_ API_KEY ExaToolssearches the web, and itscategorycan belinkedin profile. It finds LinkedIn profile pages with your Exa key.- Pipedream's LinkedIn MCP server works on a LinkedIn account you connect. Its member profile action takes a person's LinkedIn id, not a profile URL.
Before you start
- Python 3.9 or later. We tested on 3.12.
- Agno with its MCP extra, OpenAI's SDK for the model, and httpx for the function:Without
pip install "agno[mcp]" openai httpx[mcp], pip installed no fastmcp in our test, andMCPToolsstopped before it connected, withImportError: `fastmcp` not installed. MCPTools builds its connections with it.The function doesn't need it. - A Datacircle API key: Log in at datacircle.dev/login with your work email. Your API key is on the page once you're in. Put it in
DATACIRCLE_. You get a $5 credit, enough for 2,105 profiles through Up2Data.API_KEY - An OpenAI API key in
OPENAI_, forAPI_KEY OpenAIResponses, the model Agno's MCP pages use. You can swap in any model Agno supports that calls tools.
The MCP way: our MCP server through MCPTools
Agno loads a remote MCP server's tools with MCPTools, as its MCP page shows. headers sends your key to our server as a Bearer token, with each request. Our MCP server is at https://. It gets a LinkedIn profile from its URL, through Up2Data, HarvestAPI or Fetchin. Save this file as mcp_ and run python mcp_agent.py:
import asyncio
import os
from agno.agent import Agent
from agno.models.openai import OpenAIResponses
from agno.tools.mcp import MCPTools
async def main():
async with MCPTools(
url="https://api.datacircle.dev/mcp",
transport="streamable-http",
headers={"Authorization": f"Bearer {os.environ['DATACIRCLE_API_KEY']}"},
include_tools=["get_linkedin_profile"],
timeout_seconds=60, # our API can take up to 45 seconds, and the default is 10
) as datacircle:
agent = Agent(
model=OpenAIResponses(id="gpt-5.2"),
tools=[datacircle],
instructions="Get a profile with get_linkedin_profile. If it answers 429, call it again with provider fetchin.",
tool_call_limit=3,
)
response = await agent.arun("What is the current job title on https://www.linkedin.com/in/example-profile?")
print(response.content)
asyncio.run(main())The server has other tools, such as get_, and include_tools keeps get_ alone. Check the tool name's spelling: with a name our server doesn't have, Agno logged Failed to initialize MCP toolkit and ran the agent with no tool. The tool returns our JSON as text: the provider's whole answer, every job and school included, plus datacircle_meta, what the call cost and your balance after it. Agno gives it to the model in full. We sent a Fetchin answer of 65,229 characters, which we saved from an earlier test of our API, and our model got all of it:
{"data": {…}, "meta": {…}, "datacircle_meta": {…}}Agno sends the model each tool's name, description and inputs, and leaves out its output schema. Our server describes the answer in a long schema, and none of it reached the model: Agno's first request, with the tool, was 1,843 bytes.
The tool takes url, and provider: up2data (the default), harvestapi or fetchin. When our scripted model sent url alone, our stand-in server used Up2Data, our server's default. At Up2Data's limit, the server tells your agent to call again through Fetchin or HarvestAPI. Agno passes our error to the model: it reads Error from MCP tool, then our error JSON inside the MCP SDK'sTextContent, and the agent keeps running. We scripted our model to call again with fetchin at Up2Data's 429, and it got the profile. HarvestAPI costs more per 1,000 profiles than the other two. Our MCP server docs list every tool.
Error from MCP tool 'get_linkedin_profile': [TextContent(type='text', text='{"error": …}', annotations=None, meta=None)]Give MCPTools more than 10 seconds
MCPTools waits 10 seconds for each call by default, and our API waits up to 45 seconds for a provider. We left out timeout_seconds and made our stand-in server answer in 50 seconds. After 10 seconds the model read this, and never got the profile:
MCP tool 'get_linkedin_profile' failed: Request 'tools/call' timed out. The MCP server may be unreachable or the request timed out.MCPTools then told our server it cancelled the call. Our stand-in server answered at 50 seconds, too late for the model. With timeout_seconds=60, as in the file above, the model got the profile.
Use async with, so a wrong key fails before the model runs
With a wrong key, MCPTools raised as it entered the async with block. Our server answered the first request with a 401, and the script stopped before any model call. We tried it on api.datacircle.dev:
mcp.shared.exceptions.MCPError: invalid API key or access tokenAgno's MCP page shows connect() and close() in a try block. We ran the same file that way with a wrong key. Agno logged the error and went on: the model got no tool and answered without the profile.
ERROR Failed to connect to <MCPTools id=mcp-api-datacircle-dev-mcp name=mcp_api_datacircle_dev_mcp functions=[]>: invalid API key or access tokenSet the right key in DATACIRCLE_. This code sends the key as a Bearer token, which needs no sign in. We haven't tried an OAuth sign in through MCPTools.
The function way: one Python function that calls our API
You send the provider's own request to api.datacircle.dev, with your Datacircle key. That's the only change. The function sends Up2Data's and Fetchin's own requests, with X-Data-Provider naming the provider. Up2Data's request is in our API reference. Save this file as tool_ and run python tool_agent.py:
import json
import os
import time
import httpx
from agno.agent import Agent
from agno.models.openai import OpenAIResponses
API = os.environ.get("DATACIRCLE_API_URL", "https://api.datacircle.dev")
KEY = os.environ["DATACIRCLE_API_KEY"]
def call(provider, method, path, **request):
"""One call to Datacircle's API through one provider. Fetchin's 429 is its rate limit: wait a second and send it again."""
for wait in (0, 1, 2):
time.sleep(wait)
answer = httpx.request(method, f"{API}{path}", headers={"Authorization": f"Token {KEY}", "X-Data-Provider": provider}, timeout=60, **request)
if provider != "fetchin" or answer.status_code != 429:
return answer
return answer
def get_linkedin_profile(url: str) -> str:
"""Get the current job title, company, location and headline on a LinkedIn profile, from the profile's URL.
Returns JSON with the keys job_title, company, location and headline, or with one key, error, when the
profile can't be read. Each call asks the provider live and is billed to the Datacircle balance.
Args:
url: The profile's LinkedIn URL, like https://www.linkedin.com/in/example-profile
"""
answer = call("up2data", "POST", "/v1/profiles/enrich", json={"url": url})
if answer.status_code == 200:
profile = answer.json()["data"]
company = profile.get("current_company") or {}
return json.dumps({"job_title": company.get("title"), "company": company.get("name"),
"location": (profile.get("location") or {}).get("raw"), "headline": profile.get("headline")})
if answer.status_code == 429: # Up2Data's daily limit or rate limit: Fetchin answers instead
answer = call("fetchin", "GET", "/api/v1/profile", params={"profileUrlOrUrn": url})
if answer.status_code == 200:
profile = answer.json()
return json.dumps({"job_title": profile.get("jobTitle"), "company": profile.get("companyName"),
"location": profile.get("location"), "headline": profile.get("title")})
if answer.status_code in (404, 422):
return json.dumps({"error": "This LinkedIn profile is private or deleted."})
if answer.status_code == 400:
return json.dumps({"error": "This is not a LinkedIn profile URL. Send one like https://www.linkedin.com/in/example-profile"})
if answer.status_code == 402:
return json.dumps({"error": "The Datacircle balance is too low for this call. Tell the user to add funds on their Datacircle dashboard."})
if answer.status_code in (429, 500, 502, 503, 504):
return json.dumps({"error": f"The provider didn't answer ({answer.status_code}), and the call wasn't charged. Try again in a minute."})
answer.raise_for_status() # 401: DATACIRCLE_API_KEY is wrong
agent = Agent(model=OpenAIResponses(id="gpt-5.2"), tools=[get_linkedin_profile], tool_call_limit=3)
print(agent.run("What is the current job title on https://www.linkedin.com/in/example-profile?").content)Agno turns any Python function in tools into a tool. The model gets the function's name, the docstring above Args as its description, and each line under Args as an input's description. Agno leaves out a Returns section, so we name the JSON's keys in the docstring's first lines.
The function sends the URL to Up2Data. At Up2Data's daily limit or rate limit (its 429), it sends the same URL to Fetchin. Fetchin takes 5 requests a second across all our customers, and returns a 429 past that: the function waits one second and sends it again, then waits two seconds and sends it once more.
The model gets four fields back, as JSON text:
{"job_title": …, "company": …, "location": …, "headline": …}The function returns json. of a dict. Agno gives the model what a function returns as text, through Python's str(). We tried the same function returning dicts, and the model read Python's notation, with single quotes:
{'job_title': …, 'company': …, 'location': …, 'headline': …}For a private or deleted profile, a URL that isn't a profile, a low balance or a provider error, the function returns JSON with one key, error, so the agent can tell the user and go on. Each field comes from the same JSON path as in our Python post:
| Field | Up2Data's answer | Fetchin's answer |
|---|---|---|
job_ | data. | jobTitle |
company | data. | companyName |
location | data. | location |
headline | data. | title |
With a wrong key (401), the function raises. Agno logs a warning with the traceback, gives the error's text to the model as the tool's result, and continues the run. We tried a wrong key on api.datacircle.dev, and the model read:
Client error '401 Unauthorized' for url 'https://api.datacircle.dev/v1/profiles/enrich'
For more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/401A 401 costs nothing. Set the right key in DATACIRCLE_. Agno sets no time limit on a function, and httpx waits 60 seconds here: the model got the answer our stand-in server sent after 50 seconds.
DATACIRCLE_ is for tests: point it at a stand-in for our API, and you can run the function without spending your balance.
Cap the agent's calls with tool_call_limit
Each call to the tool is a call to our API. We bill each call that gets a profile, and each call for a profile Fetchin or HarvestAPI can't find, at the price on our pricing page. In the function way, one call to the tool sends up to four requests, and we bill at most one of them: Up2Data's 429 and Fetchin's 429 are free.
tool_call_limit=3 runs the tool 3 times at most in one run, as Agno's tool call limit page says. In our test, a model that asked for the tool at every turn got 3 profiles, then this answer to each further call:
Tool call limit reached. Tool call get_linkedin_profile not executed. Don't try to execute it again.Model calls have no limit. Agno asks the model again after each of those answers, with the tool still on offer. Our scripted model ignored the line and kept asking, and Agno kept calling the model: 17,748 model calls in 9 minutes, until we stopped it, and still 3 calls to our API. We haven't tried that line on a real model, and your model's provider bills each model call. Say in your task how many profiles the agent may look up.
API answers: cost and what the tool returns
| Answer | Meaning | Cost | The tool returns |
|---|---|---|---|
| Up2Data 200 | the profile | $2.375 per 1,000 | the four fields |
| Up2Data 422 | the profile is private or deleted | free | error: "This LinkedIn profile is private or deleted." |
| Up2Data 400 | not a LinkedIn profile URL | free | error: "This is not a LinkedIn profile URL." |
| Up2Data 429 | its daily limit or rate limit | free | Fetchin's answer |
| Fetchin 200 | the profile | $1.485 per 1,000 | the four fields |
Fetchin 404 with PROFILE_ | the profile is private or deleted | $1.485 per 1,000: Fetchin bills the lookup | error: "This LinkedIn profile is private or deleted." |
| Fetchin 429 | its rate limit, which all our customers share | free | Fetchin's answer to a second or third try, or error: "Try again in a minute" |
| 402 | your balance can't cover the call | free | error: "The Datacircle balance is too low for this call." |
| 502, 503 or 504 | the provider failed, or didn't answer within 45 seconds | free | error: "Try again in a minute" |
| 401 | your key is wrong | free | an exception the model reads |
Up2Data takes $1 a day per account (421 profiles), with a shared daily limit for all customers, then answers 429 until 00:00 UTC. HarvestAPI has no daily limit. Fetchin has no daily limit either.
The tests we ran
- We ran these tests on October 11, 2026, on Python 3.12, with
agno3.1.2 and the fastmcp 4.1.0 andmcp2.3.0 it installs, openai 3.28.0 and httpx 0.28.1. - We ran each file above through a real Agno agent, then the variants each section names. We pointed
OPENAI_at a stand-in for OpenAI's API, which we scripted to ask forBASE_URL get_once, to ask again throughlinkedin_ profile fetchinwhen the MCP tool returned 429, then to answer from the tool's reply. For thetool_call_limittest, we scripted it to ask at every turn. - The function ran against a stand-in server on our machine that answers like our API: the example 200 answers for Up2Data and Fetchin from our API reference, then each error in the table. Each time, the model got what the table lists for that answer.
- The MCP agent ran against a stand-in that answers like our MCP server: the model got the profile, each of our errors in the
Error from MCP toolline above, and Fetchin's profile after Up2Data's 429. We delayed answers by 50 seconds, sent the long Fetchin answer, and tried a tool name our server doesn't have. - We called api.datacircle.dev with a wrong key. Our API answered the function with a 401, which the model read. The MCP agent stopped at the error above, before it called a model. These calls cost nothing.
- We didn't run the Bright Data or Exa toolkit, or any test with a real Datacircle key or a real model, or try an OAuth sign in.
Cost per 1,000 profiles
We charge your balance the prices on our pricing page, with no markup:
| Up2Data | HarvestAPI | Fetchin | |
|---|---|---|---|
| Per 1,000 found | $2.375 | $3.70 | $1.485 |
| Per 1,000 not found | free | $2.30 | $1.485 |
| Daily limit | 421 profiles per account | none | none |
Say your agent looks up 1,000 profiles in a day, one call each, and the providers find every one. You pay us at most $1.86: $1.00 for 421 through Up2Data and $0.86 for the other 579 through Fetchin. Your agent may call the tool more than once per question. We bill each call that gets a profile, and each lookup Fetchin or HarvestAPI can't find. Your model's provider bills you for its tokens.
The same tool in other agent frameworks: LinkedIn profiles in LangChain, LinkedIn profiles in Pydantic AI, LinkedIn profiles in Google ADK, LinkedIn profiles in the Claude Agent SDK, LinkedIn profiles in smolagents and LinkedIn profiles in Microsoft Agent Framework. The same call from a Python script: get LinkedIn profile data with Python. In Claude, ChatGPT or Cursor: our LinkedIn MCP server. Other vendors' prices per 1,000: LinkedIn profile API pricing compared.
Questions
Does Agno have a LinkedIn tool?
Not one of its own. Its Bright Data toolkit reads a LinkedIn profile through Bright Data's API, with your Bright Data key, and Exa can search for LinkedIn profiles. To read profiles through us, load our MCP server at https://
How do I send an API key to a remote MCP server with Agno's MCPTools?
Pass it in headers: MCPTools(url="https://
Why does MCPTools say "Request 'tools/call' timed out"?
MCPTools waits 10 seconds for each tool call by default, and an API call can take longer. Pass timeout_seconds=60 to MCPTools. In our test, a 50 second answer then reached the model.
Why does MCPTools fail with "fastmcp not installed"?
pip install agno doesn't install fastmcp, the library MCPTools connects with. Install "agno[mcp]". We tested agno 3.1.2 with the fastmcp 4.1.0 it installs.
How much does a LinkedIn profile cost?
$2.375 per 1,000 through Up2Data (a profile it can't find is free), $3.70 per 1,000 through HarvestAPI. $1.485 per 1,000 through Fetchin, a profile it can't find billed the same. HarvestAPI bills a profile it can't find at $2.30 per 1,000.
Is there a daily limit?
Up2Data takes $1 a day per account (421 profiles), with a shared daily limit for all customers, then answers 429 until 00:00 UTC. HarvestAPI has no daily limit. Fetchin has no daily limit either.
Is each request live, or cached?
Live. Each request goes to the provider and gets the profile as it is today.
Do I need a LinkedIn account?
No. You send the profile's URL with your Datacircle key: no LinkedIn login, no cookies, no browser.
What happens when my balance runs out?
A call your balance can't cover answers 402. Add funds, from $5, on your dashboard.
Get started
Sign up at datacircle.dev with your work email: a $5 credit, that's 2,105 LinkedIn profiles at $2.375 per 1,000.
Sign up