Datacircle

LinkedIn profiles in LangChain: a LinkedIn tool for your agent, through MCP or @tool

You're building an agent with LangChain in Python, 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 a record's job title current. LangChain's tool integrations page lists no tool that does this. LinkedIn appears there once: CrustAPI, a web search tool, lists LinkedIn among its search results.

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 give your agent that tool in two ways. With our MCP server, you write no tool function, only the code that connects to it. A @tool function that calls our API lets you choose what the model sees. 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 costs less, $1.485 per 1,000, but it bills a profile it can't find at the same price, and all our customers share its rate limit. So the function calls Fetchin only when Up2Data answers 429, at its daily limit or its rate limit. At Up2Data's limit, the server tells your agent to call again through Fetchin or HarvestAPI. You send the provider's own request to api.datacircle.dev, with your Datacircle key. That's the only change.

We ran the @tool function against a stand-in server that answers like our API, and inside an agent with a scripted stand-in for the model. We called api.datacircle.dev with a wrong key, from the @tool 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, which LangChain needs.
  • 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_API_KEY. You get a $5 credit, enough for 2,105 profiles through Up2Data.
  • A chat model that calls tools. The code below names openai:gpt-5.5, from LangChain's examples. It needs langchain-openai and an OpenAI key. Swap in your own model and package.

The MCP way: our MCP server through MCPAdapter

In LangChain, you load an MCP server's tools with MCPAdapter, from the langchain.mcp package. LangChain marks it as beta, and it prints a LangChainBetaWarning once. Our MCP server is at https://api.datacircle.dev/mcp. It gets a LinkedIn profile from its URL, through Up2Data, HarvestAPI or Fetchin. The first time, your client signs you in with your Datacircle email (OAuth). Or send your API key as a Bearer token. Install LangChain with the mcp extra:

pip install "langchain[mcp]" langchain-openai
import asyncio
import os

from fastmcp.client import Client
from langchain.agents import create_agent
from langchain.mcp import MCPAdapter


async def main():
    client = Client("https://api.datacircle.dev/mcp", auth=os.environ["DATACIRCLE_API_KEY"])
    async with MCPAdapter(client) as adapter:
        tools = [tool for tool in await adapter.list_tools() if tool.name == "get_linkedin_profile"]
    agent = create_agent(
        model="openai:gpt-5.5",  # your choice of chat model
        tools=tools,
        system_prompt="Answer questions about the current role on a LinkedIn profile. Get the profile with get_linkedin_profile.",
    )
    result = await agent.ainvoke({"messages": [{"role": "user", "content": "What is the current job title on https://www.linkedin.com/in/williamhgates?"}]})
    print(result["messages"][-1].content)


asyncio.run(main())

The Client sends your key as Authorization: Bearer, as LangChain's authentication page shows, so no browser opens to sign you in. The Client connects to the URL over Streamable HTTP, the transport our server uses. The server has other tools, such as get_balance, and the filter keeps get_linkedin_profile alone. The async with block connects to list the tools. Each tool opens its own connection when the agent calls it, so the agent works after the block ends.

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. A failed call reaches the model as a ToolMessage with status="error", holding our API's error, as LangChain's MCP tools page says. At Up2Data's limit, the server tells your agent to call again through Fetchin or HarvestAPI. The model picks which one, and HarvestAPI costs more per 1,000 profiles. Our MCP server docs list every tool.

If your project uses langchain-mcp-adapters

Its repository says "This repository is no longer actively maintained", and LangChain's migration guide says langchain.mcp replaces its MultiServerMCPClient. Add our server to the config of MultiServerMCPClient, with the key in headers. This file builds the same agent as above:

import asyncio
import os

from langchain.agents import create_agent
from langchain_mcp_adapters.client import MultiServerMCPClient


async def main():
    client = MultiServerMCPClient({
        "datacircle": {
            "transport": "http",
            "url": "https://api.datacircle.dev/mcp",
            "headers": {"Authorization": f"Bearer {os.environ['DATACIRCLE_API_KEY']}"},
        },
    })
    tools = [tool for tool in await client.get_tools() if tool.name == "get_linkedin_profile"]
    agent = create_agent(
        model="openai:gpt-5.5",  # your choice of chat model
        tools=tools,
        system_prompt="Answer questions about the current role on a LinkedIn profile. Get the profile with get_linkedin_profile.",
    )
    result = await agent.ainvoke({"messages": [{"role": "user", "content": "What is the current job title on https://www.linkedin.com/in/williamhgates?"}]})
    print(result["messages"][-1].content)


asyncio.run(main())

langchain-mcp-adapters needs an older version of the mcp package than langchain[mcp] does, so you can't install both in one Python environment.

The @tool way: one function that calls our API

Install LangChain and requests, then save the code below as datacircle_tool.py:

pip install langchain requests langchain-openai
import os
import time

import requests
from langchain.tools import tool

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


@tool
def get_linkedin_profile(url: str) -> dict | str:
    """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: 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 "This LinkedIn profile is private or deleted."
    if answer.status_code == 400:
        return "This is not a LinkedIn profile URL. Send one like https://www.linkedin.com/in/williamhgates"
    if answer.status_code == 402:
        return "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 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

LangChain's @tool turns the function into a tool. The tool's name is the function'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. When Up2Data answers 429, at its daily limit or its rate limit, the function sends the same URL to Fetchin. Fetchin takes 5 requests a second from all our customers together, and answers 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:

{"job_title": …, "company": …, "location": …, "headline": …}

For a private or deleted profile, a non-profile URL, a low balance or a provider error, the tool returns a sentence instead, so the agent can tell the user and go on. With a wrong key (401), the tool raises an error and stops the run: set the right key in DATACIRCLE_API_KEY. Each field comes from the same JSON path as in our Python post:

Source of each field
FieldUp2Data's answerFetchin's answer
job_titledata.current_company.titlejobTitle
companydata.current_company.namecompanyName
locationdata.location.rawlocation
headlinedata.headlinetitle

DATACIRCLE_API_URL is for tests: point it at your own server that answers like ours, and you can run the tool without spending your balance. We tested the tool against a server like that.

The agent

LangChain's create_agent gives a chat model your tools and runs the loop: the model asks for a tool, reads the tool's answer, and replies. With the @tool function:

from langchain.agents import create_agent

from datacircle_tool import get_linkedin_profile

agent = create_agent(
    model="openai:gpt-5.5",  # your choice of chat model
    tools=[get_linkedin_profile],
    system_prompt="Answer questions about the current role on a LinkedIn profile. Get the profile with get_linkedin_profile.",
)
result = agent.invoke({"messages": [{"role": "user", "content": "What is the current job title on https://www.linkedin.com/in/williamhgates?"}]})
print(result["messages"][-1].content)

You pick the model: any LangChain chat model that calls tools, as a "provider:model" string or a model object. The MCP way above builds the same agent, with await agent.ainvoke.

Each answer from our API: its cost and what the @tool function returns

Our API's answers to the @tool function
AnswerWhat it meansCostThe tool returns
Up2Data 200the profile$2.375 per 1,000the four fields
Up2Data 422Up2Data can't reach that profile: private or deletedfree"This LinkedIn profile is private or deleted."
Up2Data 400not a LinkedIn profile URLfree"This is not a LinkedIn profile URL."
Up2Data 429Up2Data's daily limit, or its rate limitfreeFetchin's answer
Fetchin 200the profile$1.485 per 1,000the four fields
Fetchin 404 with PROFILE_NOT_FOUNDFetchin can't find that profile: private or deleted$1.485 per 1,000: Fetchin bills the lookup"This LinkedIn profile is private or deleted."
Fetchin 429its rate limit, which all our customers sharefreeFetchin's answer to a second or third try, or "Try again in a minute"
402your balance can't cover the callfree"The Datacircle balance is too low for this call."
502, 503 or 504the provider failed, or didn't answer within 45 secondsfree"Try again in a minute"
401your key is wrongfreean error that stops the run

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.

How we tested the code

  • We ran these tests on October 11, 2026, on Python 3.12, with langchain 1.4.4 and fastmcp 4.1.0, and in a second environment langchain-mcp-adapters 0.3.2.
  • The @tool function, as it is above, against a stand-in server on our machine that answers like our API: Up2Data's and Fetchin's 200 from our API reference's examples, then each error in the table. Each call returned what the table says.
  • We ran both agents with a scripted stand-in for the model, which asks for get_linkedin_profile. The stand-in got the tool's answer as a ToolMessage. We ran both MCP examples against a stand-in that answers like our MCP server, and each listed and called get_linkedin_profile.
  • We called api.datacircle.dev with a wrong key: our API answered the @tool function with a 401 and {"error": "invalid api key"}. The MCP code raised MCPError: invalid API key or access token, and MultiServerMCPClient raised httpx's 401 Unauthorized. None of these calls cost anything.
  • We didn't run any of these tests 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:

Our price per 1,000 profiles and daily limit, by provider
Up2DataHarvestAPIFetchin
Per 1,000 found$2.375$3.70$1.485
Per 1,000 not foundfree$2.30$1.485
Daily limit421 profiles per accountnonenone

Say your agent looks up 1,000 profiles in a day with the @tool function, 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. The model may call the tool more than once for a question, and we bill each call. Your model's provider sends you its own bill for tokens.

The same call from a Python script: get LinkedIn profile data with Python. In Node.js: get LinkedIn profile data with Node.js. In an n8n workflow: LinkedIn profiles in n8n. In a CrewAI crew: LinkedIn profiles in CrewAI. In Claude, ChatGPT or Cursor: our LinkedIn MCP server. Other vendors' prices per 1,000: LinkedIn profile API pricing compared.

Questions

Does LangChain have a LinkedIn tool?

LangChain's tool integrations page lists no tool that gets a LinkedIn profile from its URL. LinkedIn appears there once: CrustAPI, a web search tool, lists LinkedIn among its search results. You can give your agent one: our MCP server at https://api.datacircle.dev/mcp, through LangChain's MCPAdapter, or a @tool function that sends POST {"url": "<the profile's LinkedIn URL>"} to https://api.datacircle.dev/v1/profiles/enrich, with the headers Authorization: Token <your key> and X-Data-Provider: up2data.

How do I connect a LangChain agent to an MCP server with an API key?

Install langchain[mcp], then give MCPAdapter a FastMCP Client with your key as auth: Client("https://api.datacircle.dev/mcp", auth=<your key>) sends it as Authorization: Bearer <your key>. The adapter's list_tools() returns the server's tools as LangChain tools to pass to create_agent.

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
Ask AI about Datacircle

Each opens with our question