Skip to main content
Glama
boxed-dev

inprofile

Official
by boxed-dev

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 once

The 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 session

A 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 it

Enter 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

source_input, target_url

what you asked for, and the URL it resolved to

identity

name, pronouns, headline, location, about, photo URL, profile URL, vanity name, followers, connections

experiences

title, company, company URL, employment type, start, end, duration, location, workplace type, description

education

school, school URL, degree, field, start, end, description

skills

list of names

languages

name, proficiency

certifications

name, issuer, issuer URL, issued, expires, credential ID, credential URL

unavailable_sections

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

auth_expired

run inprofile auth with a fresh cookie

2

checkpoint

LinkedIn wants a security challenge solved; sign in with your browser, then run inprofile auth

3

not_found

check the username or URL

4

network

retry

5

parse_failed

LinkedIn changed its payload; the message names what could not be read

7

rate_limited

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

linkedin-mcp-server

linkedin_scraper

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 (send_message, connect_with)

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. httpx and pydantic are 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 get exits 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 read

Tests 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.

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.

Maintenance

ActivityMaintained
ResponsivenessNo issues

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

Related MCP Servers

Latest Blog Posts

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