LinkedIn profiles in Google ADK: a LinkedIn tool through MCP or a function tool
You're building an agent with Google's Agent Development Kit (ADK), and it 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. ADK has no LinkedIn tool: its own tools search Google, read a web page or run code. Its marketing integrations list LinkedIn only as an ad platform. We searched its GitHub repository on October 11, 2026: LinkedIn comes up only in one sample's test messages.
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. Right now we have 3 live LinkedIn profile APIs that we trust: Up2Data, HarvestAPI and Fetchin.
You can connect your agent to our MCP server 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, unless your agent names another provider through MCP. 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 bills a profile it can't find, and all our customers share its rate limit. The function sends a URL to Fetchin only when Up2Data hits its daily limit or rate limit and returns a 429.
We ran both agents with adk run, with a scripted test 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.
Before you start
- Python 3.10 or later.
- ADK with MCP support, and requests for the function:
pip install "google.-adk[mcp]" requests - 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. You get a $5 credit, enough for 2,105 profiles through Up2Data.
- A Gemini API key. The code uses
gemini, as ADK's Python quickstart does. Any model ADK can use with tools works too.-flash -latest
The agent folder
ADK runs an agent from a folder. __ imports agent.py, and agent.py defines root_. Both ways below use this folder, with a different agent.py:
linkedin_agent/
__init__.py
agent.py
.env__ holds one line:
from . import agent.env holds your two keys. adk run reads it before it loads agent.py, so the code finds DATACIRCLE_ there:
GOOGLE_API_KEY=<your Gemini API key>
DATACIRCLE_API_KEY=<your Datacircle API key>From the folder that holds linkedin_, ask the agent a question:
adk run linkedin_agent "What is the current job title on https://www.linkedin.com/in/williamhgates?"adk web, run from the same folder, opens a chat with the agent in your browser.
The MCP way: our MCP server through McpToolset
ADK reads a remote MCP server's tools with McpToolset, as ADK's MCP tools page shows. Our MCP server is at https://. It gets a LinkedIn profile from its URL, through Up2Data, HarvestAPI or Fetchin. It reads your key from the Authorization header as a Bearer token. This agent.py is the whole agent:
import os
from google.adk.agents import Agent
from google.adk.tools.mcp_tool import McpToolset, StreamableHTTPConnectionParams
datacircle = McpToolset(
connection_params=StreamableHTTPConnectionParams(
url="https://api.datacircle.dev/mcp",
headers={"Authorization": f"Bearer {os.environ['DATACIRCLE_API_KEY']}"},
),
tool_filter=["get_linkedin_profile"],
)
root_agent = Agent(
model="gemini-flash-latest",
name="linkedin_agent",
instruction="Answer questions about the current role on a LinkedIn profile. Get the profile with get_linkedin_profile.",
tools=[datacircle],
)The server has other tools, such as get_, and tool_filter keeps get_ alone. There's no timeout to set: McpToolset waits up to 5 minutes for a tool's answer, and our API waits up to 45 seconds for the provider. To check the client's time limit, we made a stand-in server answer in 50 seconds: the model got the profile.
The tool takes url, and provider: up2data (the default), harvestapi or fetchin. It returns the provider's whole JSON, every job and school included, plus datacircle_meta: what the call cost and your balance after it. ADK gives the model the whole MCP answer, so the model reads the profile twice: once as text, once as structuredContent:
{"content": [{"type": "text", "text": "{\"data\": …}"}], "isError": false,
"structuredContent": {"data": {…}, "meta": {…}, "datacircle_meta": {…}}}To have the model read four short fields instead, use the function below. At Up2Data's limit, the server tells your agent to call again through Fetchin or HarvestAPI. The model picks one, and HarvestAPI costs more per 1,000 profiles than the other two. Our MCP server docs list every tool.
ADK hands our errors to the model as a tool answer, and the run goes on:
{"content": [{"type": "text", "text": "{\"error\": …}"}], "isError": true}A wrong key doesn't stop the run. ADK can't list our tools, so the agent runs without them, and the model answers with no profile. You see no error on the screen. adk run prints the path of its log as it starts, and the log says will run without the tools from toolset McpToolset, then invalid API key or access token. Set the right key in DATACIRCLE_.
The function way: one 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 own request, with X-Data-Provider naming the provider. This agent.py holds the function and the agent:
import os
import time
import requests
from google.adk.agents import Agent
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 = requests.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) -> dict:
"""Get the current job title, company, location and headline on a LinkedIn profile, from the profile's URL,
like https://www.linkedin.com/in/williamhgates. Each call asks the provider live and is billed to the Datacircle balance."""
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 {"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 {"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 {"error": "This LinkedIn profile is private or deleted."}
if answer.status_code == 400:
return {"error": "This is not a LinkedIn profile URL. Send one like https://www.linkedin.com/in/williamhgates"}
if answer.status_code == 402:
return {"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 {"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
root_agent = Agent(
model="gemini-flash-latest",
name="linkedin_agent",
instruction="Answer questions about the current role on a LinkedIn profile. Get the profile with get_linkedin_profile.",
tools=[get_linkedin_profile],
)An ADK Agent takes a plain Python function in tools, as ADK's function tools page shows: the function's name is the tool's, its docstring is the description the model reads, and the type hint on url is its input. The function sends the URL to Up2Data, and to Fetchin when Up2Data returns a 429. 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 function returns a dict, as ADK's docs advise, and ADK gives it to the model as it is:
{"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 dict holds one error sentence, so the agent can tell the user and go on.
A wrong key (401) stops the run: adk run prints Error: 401 Client Error: Unauthorized for url: …. Set the right key in DATACIRCLE_. 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 |
ADK runs a function that isn't async on its event loop, so while the function waits for our API, the rest of the process waits too. With adk run and one question at a time, nothing else waits. ADK's RunConfig has tool_ to run such functions in threads, which we haven't tried.
DATACIRCLE_ is for tests: point it at a mock of our API to run the tool without spending your balance. We tested the tool against a mock.
Each API answer: its cost and what the tool returns
| Answer | What it means | 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 | "This LinkedIn profile is private or deleted." |
| Up2Data 400 | not a LinkedIn profile URL | free | "This is not a LinkedIn profile URL." |
| Up2Data 429 | Up2Data's 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 | "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 "Try again in a minute" |
| 402 | your balance can't cover the call | free | "The Datacircle balance is too low for this call." |
| 502, 503 or 504 | the provider failed, or didn't answer within 45 seconds | free | "Try again in a minute" |
| 401 | your key is wrong | free | nothing: the run stops with the error |
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
google2.11.0 and the-adk[mcp] mcp2.3.0 it installs, and requests 2.34.2. - We ran each agent folder above with
adk run, its files unchanged, with a scripted model in place of Gemini. Our test code made any model name,geminiincluded, load a model we scripted to ask for-flash -latest get_once, then answer with what the tool returned. Our test code pointed the URL in the MCP file'slinkedin_ profile agent.pyat a stand-in server. - The function agent 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. With both keys set only in
.env, the run reached the stand-in and got the profile. - The MCP agent ran against a stand-in that answers like our MCP server: the model saw
get_alone, read the profile twice, and got each of our errors as a tool answer. We made one stand-in answer take 50 seconds, and the model got the profile.linkedin_ profile - We called api.datacircle.dev with a wrong key. Our API answered the function with a 401 and
{"error": "invalid api key"}, and the run stopped with the error. The MCP agent ran on with no tool, and ADK's log saidinvalid API key or access token. These calls cost nothing. - We didn't run any test with a real Datacircle key or a real model.
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 with the function, 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, and we bill each call. Google bills you for the model's tokens.
The same tool in other agent frameworks: LinkedIn profiles in LangChain, LinkedIn profiles in CrewAI, LinkedIn profiles in the OpenAI Agents SDK, LinkedIn profiles in LlamaIndex and LinkedIn profiles in Pydantic AI. 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 Google ADK have a LinkedIn tool?
No. Its own tools search Google, read a web page or run code, and none reads a LinkedIn profile. To use ours, add our MCP server at https://
How do I connect a Google ADK agent to an MCP server with an API key?
Create McpToolset(connection_
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