inprofile
OfficialClick on "Install Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@inprofileGet the LinkedIn profile for averystone"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
inprofile
Read a LinkedIn profile into structured JSON, using the session cookie from the browser you already signed in to. No browser automation, no API keys, no scraping service, no credentials handed to anyone. Everything runs on your machine.
$ inprofile get averystone # abridged; null fields omitted here, not in the output
{
"source_input": "averystone",
"target_url": "https://www.linkedin.com/in/averystone/",
"identity": {
"full_name": "Avery Stone",
"headline": "Chair, Northwind Foundation and Founder, Helio Energy",
"location": "Seattle, Washington, United States",
"about": "Chair of the Northwind Foundation. Founder of Helio Energy. ...",
"profile_url": "https://www.linkedin.com/in/averystone/",
"followers": 1204663
},
"experiences": [
{
"title": "Co-chair",
"company": "Northwind Foundation",
"company_url": "https://www.linkedin.com/company/1122334/",
"start": "2000",
"end": null
}
],
"education": [{ "school": "Ashford University", "start": "1973", "end": "1975" }],
"skills": [],
"languages": [],
"certifications": [],
"unavailable_sections": []
}Install
Requires Python 3.11 or newer and a LinkedIn account whose interface language is English.
Not on PyPI yet. Install from the repository:
git clone https://github.com/boxed-dev/inprofile
cd inprofile
uv tool install . # or: uv sync, then prefix commands with `uv run`
inprofile auth # paste your li_at cookie onceThe only dependencies are httpx and pydantic. There is no browser to install.
auth stores the cookie at ~/.inprofile/cookies.json, mode 600. To find it: sign in to LinkedIn in your own browser, open developer tools, and copy the value of the li_at cookie for linkedin.com. INPROFILE_LI_AT overrides the stored file. Repeat only when LinkedIn signs you out.
Related MCP server: LinkedIn MCP Server
Use
Command line
inprofile check # is the stored session still signed in?
inprofile get some-username # JSON on stdout
inprofile get https://www.linkedin.com/in/some-username/ --out one.json
inprofile get first-username second-username # several, one sessionA target is a username or any linkedin.com/in/... URL. The names in these examples are invented, as is the sample output above and every test fixture in the repository. --out takes a single target.
Web page
inprofile ui # serves http://127.0.0.1:8765/ and opens itEnter a target, press Fetch, read or download the JSON. The page is served from localhost only.
From an AI assistant (MCP)
uv tool install ".[mcp]" # from the cloned repository{
"mcpServers": {
"inprofile": { "command": "inprofile-mcp" }
}
}Two tools: get_profile(target) returns the JSON below, check_session() reports whether the session is still valid.
From Python
import asyncio
from inprofile import Session, scrape_profile
async def main():
async with Session() as session:
profile = await scrape_profile("some-username", session)
print(profile.identity.headline)
for job in profile.experiences:
print(job.title, job.company, job.start, job.end)
asyncio.run(main())Profile and everything inside it are Pydantic models, so profile.model_dump() and model_dump_json() work as expected.
Output
Field | Contents |
| what you asked for, and the URL it resolved to |
| name, pronouns, headline, location, about, photo URL, profile URL, vanity name, followers, connections |
| title, company, company URL, employment type, start, end, duration, location, workplace type, description |
| school, school URL, degree, field, start, end, description |
| list of names |
| name, proficiency |
| name, issuer, issuer URL, issued, expires, credential ID, credential URL |
| names of any sections that failed to load on this read |
Dates are YYYY or YYYY-MM. An end of null on an experience means the role is current. Fields LinkedIn does not show are null, never guessed. URLs that are not http or https are dropped rather than passed through.
Exit codes
Code | Name | What to do |
0 | success | |
1 |
| run |
2 |
| LinkedIn wants a security challenge solved; sign in with your browser, then run |
3 |
| check the username or URL |
4 |
| retry |
5 |
| LinkedIn changed its payload; the message names what could not be read |
7 |
| wait for the cap to clear |
Rate limits
LinkedIn restricts accounts that browse at machine speed, and the account at risk is yours. By default inprofile allows 20 profiles per hour and 100 per day, counted in ~/.inprofile/history.json and enforced across runs. An attempt counts whether or not it returned a profile, because the requests were made either way. --no-limit skips the check; the caps exist to protect your account, so raising them is a decision, not a default.
How it works
LinkedIn renders profiles with a server-driven UI. The page arrives as a React Flight payload in window.__como_rehydration__, which the browser turns into the DOM and then largely discards on hydration — the hydrated DOM is a fraction of what the server sent. inprofile reads that payload directly, so it needs no browser and sees more than the rendered page does. CSS class names are hashed and change on every deploy, so nothing is selected by class: the parser keys on the stable component ids and on document structure.
Experience ships with its page. The remaining sections load through one SDUI pagination endpoint, which inprofile calls the same way the page would, asking for 50 rows rather than the 10 the browser requests first. flight.py turns either response back into the HTML shape the parser reads.
A profile is the profile page, details/experience, and four pagination calls, spaced by a short random pause. Measured end to end through the CLI, that is roughly 15 seconds.
If a section fails to load, it is left empty and named in unavailable_sections rather than failing the whole read. Only the profile page itself is fatal.
src/inprofile/
cli.py auth, check, get, ui
client.py the signed-in HTTP session and the SDUI requests
cookies.py the stored li_at cookie
flight.py React Flight payload to HTML
extract.py HTML to Profile
dom.py minimal HTML DOM over html.parser
models.py Pydantic output types
ratelimit.py per-account fetch caps
ui.py, ui.html the local web page
mcp_server.py MCP server (optional extra)Alternatives
Two larger projects cover this ground, and both do more than inprofile does:
inprofile | |||
Stars | new | ~3.3k | ~4.5k |
Licence | MIT | Apache-2.0 | GPL-3.0 |
Covers | profiles | profiles, companies, jobs, messaging | profiles, companies, jobs, posts |
Drives a browser | no | yes (patchright) | yes |
Can act on your account | no | yes ( | no |
If you need companies, jobs, or messaging, use one of those instead. inprofile is narrower on purpose, and differs in two ways that are easy to check:
No browser.
httpxandpydanticare the only dependencies. Nothing to install, nothing to keep up to date with Chrome, and it runs where a browser will not.Read-only by construction. There is no code path that posts, connects, or messages. The worst a bug can do to your account is read a profile you did not mean to read.
Reading the server payload rather than the rendered page is also why sections come back complete: the browser discards most of what the server sent, so a profile with 36 skills returns 36.
Limitations
Worth knowing before you rely on it:
It will break. LinkedIn changes its payload without notice. When that happens
getexits 5 and names what it could not read.English only. The parser reads LinkedIn's own words: month names,
Present,Issued,followers. A profile viewed in another interface language will not parse.Tested against a modest set of profiles. Individual roles, roles grouped under one company, employers without a company page, missing sections, and unknown usernames all have fixtures.
50 rows per section. Each lazy section asks for 50 entries in one call and does not page beyond that, so a profile with more than 50 skills is truncated.
Sections not read: recommendations, volunteering, publications, projects, honours, courses, contact details behind the overlay.
Signing in is yours to do. inprofile never handles your password; it reads the cookie your own browser already holds.
Development
uv sync --all-extras
uv run pytest -q
uv run ruff check . && uv run mypy src tests
./run.sh <username> # the above, plus one live readTests run offline against fixtures in tests/fixtures/. Those keep the page structure of real captures, but every name, employer, school, identifier, and image URL in them is invented, so the repository carries nobody's profile. When LinkedIn changes something, render() in flight.py and the section ids in extract.py are where it lands. A page you capture while debugging holds real personal data, so describe the change in an issue rather than attaching it.
Legal
Read this before you run it. It is not legal advice.
LinkedIn's User Agreement prohibits what this tool does. Section 8.2 ("Don'ts"), effective 3 November 2025, says you will not "[d]evelop, support or use software, devices, scripts, robots or any other means or processes (such as crawlers, browser plugins and add-ons or any other technology) to scrape or copy the Services, including profiles and other data from the Services." Using this is covered. So is publishing it, and so is contributing to it.
Do not cite hiQ v. LinkedIn as cover. That case concerned public profiles visible without signing in, and it turned on the Computer Fraud and Abuse Act. It did not bless authenticated automated access, and LinkedIn went on to prevail on its contract claims against hiQ. This tool runs as a signed-in member, which is the other side of that line.
The realistic risks are not criminal ones. After Van Buren v. United States, a CFAA claim over reading pages your own account may already open is unlikely. What actually happens is an account restriction or termination, a takedown of a repository like this one, or a civil claim for breach of contract.
If you keep or forward the output, you take on data-protection duties. The JSON is another person's personal data. Under the GDPR that makes you a controller: you need a lawful basis, and Article 14 requires you to notify the person unless an exception applies. Other jurisdictions have their own rules. Reading a profile and closing the terminal is not the same as building a dataset.
The MCP server sends that personal data to a model. Whatever provider the calling client uses receives it. Do not wire this into recruitment screening, candidate ranking, or any other decision about a person without a lawful basis and a look at your obligations — the EU AI Act treats employment-related systems as high-risk.
Use your own account, and only your own. Section 8.2 also prohibits using "another's account
(such as sharing log-in credentials or copying cookies)". inprofile auth is for the cookie of
the account you are signed in to yourself.
The MIT licence covers the code, not your conduct. It grants you no permission to breach LinkedIn's terms. This project is not affiliated with, authorised by, or endorsed by LinkedIn.
License
MIT
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections. Dates show when Glama detected each change.
No tool schema history has been recorded yet.
This server cannot be installed
Maintenance
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Related MCP Connectors
Live LinkedIn data for AI agents: profiles, companies, jobs, posts, email finding. No account risk.
Search LinkedIn & Sales Navigator. Get structured people, companies, posts, jobs data. 4.9/5 on G2.
LinkedIn profile scraper: herkese açık profil bilgilerini toplu şekilde çıkarır ve dışa aktarır.
Stealth scraping & search. Bypasses Cloudflare, DataDome & LinkedIn via Cyborg HITL approach.
Related MCP Servers
- FlicenseNot gradedqualityDmaintenanceEnables extraction of comprehensive LinkedIn profile data including experience, education, skills, and contact information through browser automation. Requires manual LinkedIn credentials input and uses anti-detection measures for reliable scraping.5-
- AlicenseNot gradedqualityDmaintenanceEnables fetching detailed LinkedIn profile data by automating a browser session with your LinkedIn cookie to access full profiles.214MIT
- AlicenseNot gradedqualityAmaintenanceProvides LinkedIn automation via a real browser session using your own cookie, enabling profile, company, job, and post search and reading, plus sending messages and connection requests with rate limiting.2MIT
- AlicenseAqualityBmaintenanceEnables AI assistants to read LinkedIn profiles, company pages, jobs, messages, and feed through a user's logged-in browser session.19Apache 2.0
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/boxed-dev/inprofile'
If you have feedback or need assistance with the MCP directory API, please join our Discord server