Skip to main content
Glama
Sundeepg98

linkedin-mcp

by Sundeepg98

linkedin

Ein MCP-Server, der dir die Daten deines eigenen LinkedIn-Kontos als strukturierte Tool‑Ergebnisse zeigt, statt als Seiten, durch die du klicken musst.

Vierzehn seiner siebzehn Tools lesen und ändern nichts. Drei schreiben.

Bis zum 2026-08-23 sagte dieser Absatz: „Es liest. Das ist alles, was es tut. Es gibt keinen Schreibpfad in diesem Repository – nicht deaktiviert, nicht gestubbt, nicht hinter einem Flag." Das stimmte. Es war erzwungen, nicht nur behauptet – und es hörte an dem Tag auf zu stimmen, an dem linkedin_save_job ausgeliefert wurde. Ein README, das den bequemen Satz weiterführt, ist das Erste, dem ein Leser vertraut, und das Erste, das ihn in die Irre führt.

Was jetzt wahr ist:

  • Das Paket enthält genau einen Aufruf, der auf LinkedIn etwas ändern kann: einen einzelnen verankerten Klick in writes.perform. Der Quellscanner meldet ihn weiterhin – ihm wurde nicht beigebracht, nicht mehr hinzusehen – und er ist nach Pfad, Funktion und Art in einer einzeiligen Allowlist zugelassen, bei der die Tests fehlschlagen, wenn sie breiter wird.

  • Schreibvorgänge sind aus, außer du schaltest sie ein. LINKEDIN_ENABLE_WRITES=1, pro Prozess. Ein frisch geklontes Repository kann überhaupt nicht auf LinkedIn schreiben.

  • Jeder Schreibvorgang ist zwei Aufrufe. Der erste bewirkt nichts und gibt dir einen Block zum Lesen; der zweite löst aus diesem Block einen Einmal-Token ein. Der Token ist an eine Aktion an einem Ziel gebunden und erlischt nach 120 Sekunden. Das macht einen geplanten oder unbeaufsichtigten Schreibvorgang strukturell unmöglich statt bloß ungeraten.

  • linkedin_unsave_job ist gebaut, gesichert und zu Tut nichts – siehe Das, das verweigert.

  • Es reicht keine Bewerbungen ein, und das ist kein Achselzucken. linkedin_job_detail meldet apply_path – ob eine Stelle auf LinkedIn zu bewerten ist oder dich an ein externes Bewerber-System übergibt – und benennt dieses System. Die identifizierende Hälfte ist als Lesevorgang ausgeliefert. Die einreichende Hälfte ist aus einem gemessenen Grund verweigert – siehe Bewerbung: die Hälfte, die ausgeliefert wird, und die Hälfte, die es nicht wird.


Lies diesen Teil, bevor du etwas anderes tust

LinkedIns Nutzungsvereinbarung schränkt den automatisierten Zugriff auf die Website ein. Das gilt unabhängig davon, wie dieser Server gebaut ist, und nichts weiter unten ändert daran etwas.

Was dieses Design bewitktel, ist nicht die Gefährdung zu leugnen, sondern sie zu minimieren:

Entscheidung

Warum das das Risiko senkt

Nur menschensgeführt

Jeder Aufruf ist einte, die du gerade bewusst gemacht hast. Nichts läuft nach Zeitplan, nichts läuft, während du schläfst.

Eine Aktion auf einmal

Ein Seitenabruf pro Tool-Aufruf. Keine Scrollschleifen, kein autoretisches Weiterblättern, kein Fan-out.

Deine eigene Session, dein eigener Rechner, deine eigene IP

Kein Cookie geht an Dritte. Kein Proxy, keine Verbreitungs-IP, keine Headless-Farm.

Ein gewöhnlicher Browser, ein Flag

Kein Stealth-Plugin, kein User-Agent- oder Plattform-Spoofing, kein Fingerabdruck-Patching, kein Proxy, keine sich gezeigt getimten Abläufe. Es wird genau ein einzige Flag übergeben – --disable-blink-features=AutomationControlled, das verhindert, dass Blink navigator.webdriver setzt – weil LinkedIn es bei der Anmeldung prüft und ohne es ablehnt. Das ist alles, es wird beim Start von readonly.assert_launch_flags_permitted erzwungen, und tests/test_launch_boundary.py lässt den Build fehlschlagen, wenn ein drittes Flag auftaucht.

Nur deine Daten

Deine Profilansichten, deine Bewerbungen, deine gespeicherten Jobs, dein Profil, deine Benachrichtigungen. Kein Auflisten oder Abgreifen anderer Mitglieddaten.

Lesen – ausser drei benannte Schreibvorgänge

Nichts wird eingereicht, gesendet, gepostet, bestätigt, eingehend oder editiert. Das Speichern, das Entfernen und das Nicht-Mehr-Folgen sind die Ausnahmen: standardmäßig aus, eine pro Vorgang, jede von dir gegen einen Monitor bestätigt, der aus einer Live-Anfrage gebaut wurde, mit einem Token, das nur einmal gilt und in zwei Minuten verfällt. Diese App sagte bis 2026-08-23 „Nur lesen", und die Bahn wurde korrigiert, nicht still verbreitert.

*Das senkt das Risiko, beseitigt es aber nicht. Automatisierter Zugriff kann trotzdem eine Ratengrenze, eine Verifikation oder eine Maßnahme mit dem Konto auslösen – und dieses Risiko musst du übernehmen. Entschprich dafür, bevor du den Server registrierst.

Das ist kein Anti-Erkennungs-Tool, und hier ist der Beweis – nicht die Zusicherung

Das eine Chromium-Flag oben ist die Art Ding, die ein Repository aussehen lässt wie ein Umgehungs-Projekt. Es ist wert, klar zu sagen, was tatsächlich gemessen wurde, denn diese Behauptung ist nachprüfbar ist, und der Leser muss sie nicht auf Vertrauen nehmen.

Geprüft am 2026-08-24 über alle 105 erfassten Dateien:

  • No fingerprint modification: zero. Kein User-Agent-, Plattform-, Locale-, Zeitzonen-, Geolocation- oder Viewport-Spoofing; kein Device-Scale-, WebGL- oder Canvas-Patching; keine page.route-Abfanggung, keein injiziertes Init-Skript, keine extra Header, keine proxy. Jeder davon wurde namentlich im gesamten Paket durchsucht und ergab null Aufrufstellen. Eine Einschränkung zu der: add_init_script steht zweimal in readonly.py, beide Male als das eigene Muster des Scanner, um einen solchen Aufruf zu erkennen. Der Scanner benennt die Dinge, die er verbietet – deshalb enthält seine Quelle sie enthält, der Rest des Pakets nicht. partition_mutation_hits bestätigt das eigenständig: Durch eine erlaubte verändernde Aufrufstelle and zero unauthorized.

  • Keine Stealth-Abhängigkeit. Vier Abhängigkeiten, keine davon eine Verachtungs-Erkennuungs-Bibliothek, und readonly.scan_source_for_evasion liefert überall im Paket null Treffer – die einzigen Treffer sind eine absichtlich eingepflanzte Kontrollgruppe in einem Test.

  • Zeitsteuerung ist fest, nicht vermenschlicht. Jede Verzögerung ist eine Konstante. import zufall kommt 0-Mal vor. Der 3‑Sekunden-Abstand zwischen Seitenläden ist MIN_INTERVAL - elapsed, genau schläft – immer maschinisch regulär. Zufälliges Jitter macht, was ein menschliches Nachahmt; ein flache Intervall ist, das zu begrenzen.

  • Das Flag selbst ist durch ein Gate durchgesetzt, nicht durch gute Absichten: readonly.assert_launch_flags_permitted wird bei jedem Start ausgeführt, und tests/test_launch_boundary` bricht den Build.

**Das Flag verhindert, dass Blink navigator.webdriver anmeldet – was LinkdIn bei der Anmeldung prüft und was einen automatisierten Browser für den eigentlichen Kontoinhaber unbrauchbar macht. Das ist alles. Einen automatisierten Browser betreibbar machen und das Umgehen der Erkennung sind zwei getrennte Dinge, und hier gibt es das.

Die Lizenz ergibt sich daraus, und sie ist bewusst nicht freigebig

Thises Repository ist proprietär – alle Rechte vorbehalten, nur zur Referenz, ohne Erlaubnis zur Nutzung, Kopie, Änderung oder Verbreitung.

Das ist kein Versehen und kein Platz halter. Dieser Server fährt eine authentifizierte LinkedIn-Sitzung unter einer Nutzungsvereinbarung, die automatisierte Zugriff. Eine durchlässige Lizenz würde Fremde einladen, sie auf ihre eigenen Konten zu richten – oder auf andere –, und mit dem Namen des Autors auf dem Repository, das die richtung angibt.

Es ist ein Portfolio-Artefakt. Es ist zum Lesen da, nicht zum Einsatz. Parsing das Design, die Grenze, die Gates und den Prüfpfad; dafür ist es da.


Related MCP server: LinkedIn MCP Server

Was es kann

Tool

Liest

linkedin_who_viewed_me

Wer Ihr Profil angesehen hat. Wo das Konto über Premium Career verfügt, reicht der Blick 365 Tage zurück – das höchste Absichtssignal einer Stellensuche.

linkedin_my_applications

Stellen, auf die Sie sich beworben haben: mit dem Status, den LinkedIn anzeigt.

linkedin_saved_jobs

Stellen, die Sie gespeichert haben.

linkedin_search_jobs

Stellensuche mit Schlüsselwörtern, Ort, Remote, Veröffentlichungsdatum und Erfahrungsgrad.

linkedin_job_detail

Eine Stellenausschreibung vollständig: die Gehaltsspanne, LinkedIns Bewerberanzahl, Arbeits- und Beschäftigungsart, Einstellungsstatus und die Ausschreibung. Keiner dieser Punkte steht auf einer Such- oder Gespeicherte-Stellen-Karte. Außerdem zeigt apply_path: welchen der beiden Bewerbungswege diese Auss Bir verwendet – und beim externen Weg, zu wessen Applicant-Tracking-System es Sie weiterleitet.

linkedin_followed_companies

Die Unternehmensseiten, denen Sie folgen, jeweils mit der numerischen ID – an diese richtet sich linkedin_unfollow_company. LinkedIn rendert nur ungefähr Zwanzig Zeilen von beliebig vielen Seiten, zeigt keine Methode zum Durchblättern der übrigen: an– dieses Werkzeug meldet also, was it in der Liste abgedeckt hat, ohne zu suggerieren, dass it it abgedeckt in decking the entire list.

linkedin_my_profile

Ihr eigenes Profil: Headline, About "About/Skills", Kenntnisse und welche Abschnitte gerendert wurden. Experience/Education/Skills aus load from LinkedIn werden erst nachgeladen, wenn the page scrollen, so support these Ausgaben als "UNKNOWN" (es handelt) not z erwartende "null" an.

linkedin_notifications

Ihre Benachrichtigungen.

linkedin_auth_status

Ob es eine aktive Sitzung gibt, was anhand einer authentifizierten Anfrage ermittelt wird.

linkedin_login_browser

Öffnet ein Fenster, in dem Sie sich selbst anmelden.

linkedin_session_info

Ob die Sitzung live ist und wann sie abläuft, aus dem eigenen Cookie-Speicher des Browsers (gelesen). Gibt die Zugangsdaten aus ndevice, the csrf seits Australian CSRFthat trägt, die Dauerhaftigkeit und warum es hier keine stille Reauthentifizierung für period die Sitzung gibt. renewal.session_lapses_at ist das Datum, nach the removal die Verlängerung nicht mehr hilft, and Sie sich von Anmeldung per man durchführen von Hand durchführen –THIS THE field compare between prior servers; in der Vergleich between servers is receivable, on LinkedIn it matches the "expiry" date of the cookie itself, because nothing in a environment like this can carry over the session listed beyond this dat.

linkedin_logout

Beenden lokalen Anmeldung, indem der Cookie-Speicher dieser Maschine geleert wird. Das einzige destruktive Werkzeug hier an: confirm=False (der Standard) führt für nothing through, only preliminary and shows as a preview of what would be done. Issues no request, LinkedIn never knows it.

linkedin_cdp_status

Wiederherstellungs-Diagnose: Gibt es einer Chrome-Instanz, an die sich dieser Server and could attach? Nichts auf LinkedIn wird angetrankt.

linkedin_server_info

Die Boundaries, the Rate-Einstellungen and the Start-Flags. No need to search the source code for it.

The right to truth – translation "The three tools".

Tool

Was funktion tut

linkedin_save_job

Setzt ein Lesezeichen für eine Ausschreibung. Aufruf without confirm_token and it doesn't perform anything: it reads the current posting alive, reads live your saved list a and shows returns a block in which the job is identified by its title and industry, in which direction toggle would-switch, where each factivative came from and how to undo the whole thing. Call them again with the token from that block, then it 's executed.

linkedin_unsave_job

Gleiche Form, dieselben gates and it verweigert – with: "siehe unten.

linkedin_unfollow_company

Die follow of a company-Page ends. As same Formique and the same five Gates. It is designed via the numerische Firmen-ID, not the name. Never by name -- names collide, change and are test – and the click is anchored in the row that itself carries the ID. it is thread you name and the thing that is clicked, giving as exact same Zeile on A construction principle. the row row to be independent by construction – design –

Nach dem klick wird das Ergebnis von einer anderen Oberfläche aus bestitigt – Ihrer gespeicherte Liste, unter Verwendung der eigenen Zählung von LinkedIn auf, itself from the other side of the table no longer its tab, not in the live Button by Klicks. performed gibt zurück true, false oder "unknown". Bei "unknown" nicht erneutholt: Ein erneuter Versuch bei einem Umschalter, der tatsächlich gesinktr hatte, bewirken the precise opposite action.

Das eine, das sich weigert

LinkedIn identifies das Speichern-Steuerelement using semantically not zugänglicher beachten ("accessible name") gehört. Bei jeder Aufnahme that City repo is in its its list resh – four postings, both hydration states, two different days – and all of them see that the label aria-label="Save the job" plus theUnzustand noted unsaved is. The name from "is saved" here the state is saved has never been observed. And can also not be determined through reads: there is nothing on account that is saved, what could serve to seeing this.

Thus:

So linkedin_unsave_job vomits anchor, but this server can't guess one. "Saved" and "Unsave the job" are both plausible, and it has seen was none of them. The refusal dazu includes dem auf which was reasonbetter about the saying "not implemented", because "not implemented" invites someone to implement it by picking string.

The fix is only one dimension line. The first directed save action creates it: The first batch, worth: perform reads nanti, the label to folder it became that changed ctrl immediately, and reports it. You write value in shape.SAVE_LABELS could, and then unsave_job receives its anchor. It is a row of the table, a single Tile of data – not a missing code path.

apply "Bewerben" - one half which with (ships) - and half that does not.

linkedin_job_detail tells tells you how about a posting allows to apply. LinkedIn shows rests that "Bewerben" not button but as a link, you really useful destination can therefore be recognized in passing without its touching at all. apply_path shows for it from three answers:

  • linkedin_apply – the Bwendung is that filled values in an submitted on LinkedIn.

  • offsite – LinkedIn Hands you are to the employer's own applicant tracking system. From there Ziel is decode that Comes out of this birthplace the Linkedin's outbound wrapper(The destination is decoded) solely from string materials: no redirect is being followed, no remote host address get contacted. You get Host, so you will know an correctly whose form you are just lying in front of you.

  • unknown – "Keine but reminder." it didn't say. A isis is a real answer, and a considerable – important, they can be no two the apparas see below.

The one that is andthe leg is valuable half, costs no other potentially an extra "page and one".load in lim and it is pure mais read.

It submits nothing. the true one you have "Bing" one goes the result. Nevertheless – it will not be both, because the Real application is under the remit of this server, but as caused a base based and measured* Reasoner,:

  1. Der Apply-Flow wurde nie aufgezeichnet. In dreizehn Job-Aufzeichnungen gibt es null Formulare, null Datei-Eingabefelder, null Dialoge, null Screening-Fragen und null Steuerelemente, die irgendetwas absenden. Nichts davon hat gesehen, was ausgefüllt oder gedrückt würde. Das ist derselbe Maßstab, an dem unsave_job gemessen wird, angewendet auf die Aktion, die ihn am meisten verdient.

  2. Eine Bewerbung kann von hier aus nicht wieder rückgängig gemacht werden, auf keiner Bestätigungsstufe, unter keinen Umständen. Ein Zurückziehen ist dauerhaft unmöglich.

  3. Der Off-Site-Teil ist nicht die Aufgabe dieses Servers, egal wie gut eine Aufnahme geworden wäre. Ein Formular zu bedienen, das jemand anderem gehört, auf einer Domain, die jemand anderem gehört, unter deren Bedingungen, ist eine anderes Software.

apply_job ist daher vollständig spezifiziert und abgesichert in writes.py, registriert kein Tool und hält keine URL, damit eine Berechtigung dafür bei der Ausgabe verweigert wird, nicht erst bei der Verwendung.

Und die Lücke hat eine Adresse, was sie zu etwas Ungemessenem statt zu etwas Dauerhaftem macht. scripts/_probe_apply_flow.py zeichnet den von LinkedIn gehosteten Flow auf und inventarisiert genau die Steuerelemente, die jeder bestehenden Aufzeichnung fehlen – Formulare, Datei-Eingabefelder, Dialoge, Screening-Fragen, das Steuerelement, das etwas Die abschickt. Es erreicht den Flow per Navigation, nicht per Klick (LinkedIn zeichnet das Apply-Steuerelement als Link), der eigene Mutations-Scanner des Pakets findet darin null mutierende Aufrufe, und wie die Job-ID als Pflichtargument entgegennimmt, sodass kein Standard für dich eine Stellenanzeige auswählt. Es liest außerdem LinkedIn genemerk den eigenen Zähler des „Applied“-Reiters vorher und nachher, weil das Öffnen eines Easy-Apply-Flows einen Draft erzeugen könnte – eine Hypothese, die niemand verifiziert hat, genau als solche gekennzeichnet und gemessen statt vermutet.

Es wurde nicht ausgeführt. Führe es mit jemandem alsAufpasser aus, an einer Stellen, deren apply_path den Wert linkedin_apply hat.

Warum der Classifier darauf besteht, dass mehrere Felder übereinstimmen, wenn ein einziges offensichtliches Feld nach nicht zu genügen habet? JederKandidat wurde gemessen, und jeder scheitert allein: data-view-views="job-apply-button" ist in einer von dreizehn Aufnahmen vorhanden und fehlt bei einem vollständig hydrierten externen Inserat vollständig – sein Fehlen trägt also gebildet gar keine Information. Der ausgehende Wrapper ist generisch – eine Aufsammlung enthält zwei davon, und nur eine ist das Apply-Steuerelementestelement. Der zugänglichbar Name ist das stärkste einzelne Feld, und er ist genau das, which LinkedIn schon geändert hat: Die Zeichenfolge „Easy Apply" kommt in keinem zugänglicher Namenaten vor, immerhin in Fließtext auf derselben Saite, so dass ein Parser, der aus der name, under dem everyone das Feature kennt, nichts findet. Und das Pre-Hydration-Payload ist makey worse als nutzlos – für ein externes Inserat wurde gemessen, dass es den eigenen Marker des alleswegs On-site-Flows trägt, für dieselbe Job-ID – denn LinkedIn versendet die gesamte Apply-State-Machine als musterte per Inserat.

Was es bewusst nicht kann

Messaging, InMail, Verbindungsanfragen. Profiländerungen. Open To Work. Einem Unternehmen folgen. Bewerstellen, liken, kommentieren, Endorsements me. Benachrichtigungen als gelesen markieren. Daten über andere Mitglieder sammeln. Bewerbungen absenden, wie im obigen Abschnitt beschrieben.

Das sind keine fehlenden Features, und es ist nicht alles die gleiche Sorte „Nein“. linkedin_server_info label deteachone as POLICY, MEASURED oder UNMEASURED, because „wir lehnen das aus prinzip ab“, „Wir haben es uns angesehen und es funktioniert nicht“ und „niemand hat es sich angesehen“ statement else, ... drei verschiedene Aussagen. Eine Liste, die eingeeinet, ist so, wie eine ungeprüfte Lücke sich wie eine Design- Ententscheidung liest.

Folgen ist das interessante, und die Begründung hat sich am 2026-08-24 geändert, ohne dass sich die Antwort ändert. Es war block, weil es keinen Die gab – dieser Server konnte den einen Zustand erzeugen, den es nicht zurücknehmen konnte. Eines existiert nun doch. Es is still not tun, because das Rückgängigmachen kann nicht gezielt werden: Eine Aufgabe nannte seinen employer nach Slug, Unfollow-Oberfläche Zeilenadressen nach numerische company id, an no no capturen in this repo carries both for "one company" on an "interm suggest user" aced. Diese Oberfläche gerenderen auch about zwanzig Zeilen von 58 front ohne Paginationssteuerung, damit der größte Teil der Liste in einem Seitenlade nicht erreichbar ist. Die Ablehnung nennt beide und nennt war pay them to lift.

Das Lesen des eigenen Posteingangs ist ungemessen, nicht verweigert. Die Lese-Grenze blockt /messaging, und jede schriftliche Rationale dafür ist gegen Senden formulier that. Whether reading is a chance was never tested. scripts/_probe_messaging.py exists of the test and testing what one usually skips: the hypothesis -- unconfirmed, and that's the point – is, that LinkedIn's Desktop-Messaging-View would open on an arrival conversation, so that a "read" of the Posteingangs would mark a Thread as read, which would be the Notifications-Objection, arriving to a tool that itself calls itself. The prose measures all contributions by reading the Nav-Badge from /feed/ went, runway and after, a surface that load does not touch. It was not run, and the forbidden list is unchanged until the is: a boundary doesn't move on an unmeasured claim.

Anything else that would change on LinkedIn's server, is out of scope, and the tests/test_readonly.py does break the build, if a second mutation call is any way inside in Packet.

One tool changes something on this machine: linkedin_logout(confirm=True) erases local Cookie-Jar. The result no request, so LinkedIn never hears it, and it is linkedin_server_info under the local_state_writes get called to the user, instead of it being folded into the read_only field.

The two side effects, stated instead of hidden

A read that changes something must be med——

  1. Opening the notifications page clears the LinkedIn's unread badge – as if it would be opened by you yourself. Measurable, not theoretical: one call on 2026-08-21 has brought it from 1 to 0, and it is doesn't come back. It can't be avoided: LinkedIn mark the list on the server as seen as soon as the page is surrendered, so there is no read of that surface with the badge sticky without safe the vacation. No click, no scroll, no appointment per single item, and no Mark-as-read call anyplace in the package. Let’s handle the badge not to empty is to not call linkedin_notifications. Since the badge disappears in the right Way anyway, each row gives unread so "LinkedIn as they had at the moment " – remaining the one fact that the page load defeats.

  2. Die Ausführung einer Suche trägt etwas hinzu deinen eigenen Verlauf der letzten Suchen – das Gleiche wie die Frage auf site erstellen.

Beide sind schon in den Doc-Strings der Protokolle – und in linkedin_server_info.


Setup

cd D:\Sundeep\projects\job-hunting\mcp-servers\linkedin
pip install -r requirements.txt
playwright install chromium
python -m pytest            # 986 passed

Dann garniere, sobald der Server bei einem client registriert ist, zuerst linkedin_login_browser auf. **Ein Fenster öffnet sich unter linkedin.com/login. Log dich dort selbst ein — dieser Server sieht, types, produces, stores and transmit passt keines ein ein Passwort. Das persistente Chrome-Profil behält Session danach, so ist es einen einmal pro—bis LinkedIn it with time.

Bestätige er mit linkedin_auth_status, before you trust verlässlichen buildup Draft.

Registrieren

stdio-Transportieren from the entry point linkedin.py:

{
  "mcpServers": {
    "linkedin": {
      "command": "python",
      "args": ["D:\\Sundeep\\projects\\job-hunting\\mcp-servers\\linkedin\\linkedin.py"]
    }
  }
}

How "read-only" is enforced – instead of asserted

linkedin_server/readonly.py four mechanisms are in, and the tests show each of them at a planted violation to fail, before they enter the real package. Check, can't fail voll; it certifies nothing.

  1. Eine Navigations-Allowlist. assert_read_url shut a single door to page.gRUEN general. Every supported URL is a patterned anchor; a typed in keyword cannot navigate to a URL-action that executes something. Blocked targets include /jobs/application/, /messaging/, invitation requests, /edit/, open-to-work, anythingwith „any kind ofaction=“ and every host expected to www.linkedin.com. The job include Post – pattern is the "tightest" on the list: it accepts a numeric ID and **impossible** Query-String um, since the url is made from an integer and thus never lived, which it must preserve. A slug from LinkedIn is (including /jobs/view/senior-node-engineer-at-acme-4600000042`) has got refused for the same cause – because a tag is a job title, and a title is a string.

  2. source-scan. The package is 3pages on calls, to begins the state -- click, fill, type, press, select-option etc, set_input_files a form submit. It searches any other GET in exactly one: the click in writes.perform.

The scanner is not relaxed, ur to allow it. It still always always awaits every mutation call, and one time the new first-line allowlist allows readonly.SANCTIONED_MUTATIONS keyed by (path, function, kind). All three parts blocking something: click the form in dom.py click in write another function write, come true fill into perform he each rejected. And a click hidden in closure one scope, because the attribution goes to the innermost enclosing function; those five near misses (catch), are shown as fail. The package where it is uses a proven method, so contains exactly so many mutation calls as number of entry list – which is what picks up a „second“ click in perform not a triple can distinguish itself of first.

evaluate has the marker: the three read-only DOM tenants with # readonly-ok tail; each new call evaluate has fails in build, until someone allows it in a reviewable diff.

  1. **A tool–surface?. **No tool has a write verb in the name, and no "Docstring" – nothing in code says it definitely does. Docstrings can be to say what a tool cannot do – „has no way to add or remove anything“ is a sentence that a read-only tool must indeed contain, so test the question using negation and not the words.

  2. The launch limit. assert_launch_flags_permitted rejects any Chromium flag outside the two allowed flag; rejects --disable-blink-features unless it carries the exact AutomationControlled – as flag toggles Blinkbehavior. You can't allow sloppy names. browser.py executes it before the good launch, so it's binding at runtime, not only in the CI. A companion Hexe rejects every "anti-detection" library, that appears as dependency (playwright_stealth, undetected_chromedrivers, captcha solvers, TLS-spoof clients), only logs on import – so this document can can can explains the boundary in prose.

The injected scripts are scanned in a separate pass, for anything that can mutate page (.click(, .value =, dispatchEvent(, fetch(-., ...). They only query DOM and read text.

That scan is bound to what's truly runs, not to what is somehow called. The tests parse package, take first argument from every page.evaluate(...), resolve to module-level constant, and scan them -- so a script cannot be inserted without also being read, and a script that these check is unable hold (for because composed at runtime) causes the build to fail at once. The previous version scanned the hand-written list of 3 names from _JS; a calm review cant hold a constant named EVIL_INLINE, enclosing localStorage.setItemasfetch(, through the whole Call site` and moved with green tests, that hole is closed. And the attack is itself now test.

Sibling server shipped of this opposite, in the day before was normal: the moment sistema.

It reports "success" the moment when session cookie appears. LinkedIn gives cookies to logged-out list visitors as well – meaning that sentence success = nothing.

Hier entsteht the verdict from GET /voyager/api/me call – identity call on LinkedIn own web app at page load. An li_at appearing is a reason to request the exchange the `endpoint at best.

The result comes in three states, not two:

  • authenticated: true -- the endpoint returned identity

  • authenticated: false – endpoint refused – or returned feed to point to "please log in" page

  • authenticated: null – not either could make. "Unknown" not "logged out", the server would tell you login again while your session entirely in order.

Corroboration can only ever turn absolutely into "false". Never allowed to generate true from "weaker assumption".

Cookie-Werte sind Anmeldedaten: Sie werden niemals protokolliert, niemals von diesem Server persistiert und erscheinen niemals in einem Tool-Ergebnis. Nur ihr Vorhandensein wird gemeldet, und zwei Tests bestätigen das.

Anmelden und wie lange es dauert

Rufen Sie linkedin_login_browser auf. Ein Chrome-Fenster öffnet sich auf der Anmeldeseite von LinkedIn, und Sie tippen hinein. Dieser Server sieht, tippt, speichert oder überträgt niemals ein Passwort – es gibt keinen Codepfad, der das könnte. Das Fenster bleibt geöffnet, bis der Identitäts-Endpunkt eine echte Sitzung bestätigt, Sie es schließen oder wait_seconds abläuft (Standard 300; übergeben Sie eine größere Zahl, wenn Sie länger brauchen).

Es ist ein einmaliger Schritt, keiner pro Sitzung. Die Sitzung lebt in einem Chrome-Profil auf der Festplatte unter _state/chrome-profile/ und übersteht daher:

Ereignis

Übersteht die Sitzung?

Warum

Dieser Server wird neu gestartet

Ja

Die Sitzung liegt auf der Festplatte, nicht im Prozess.

Die Maschine wird neu gestartet

Ja

Ebenfalls.

Das Profilverzeichnis wird gelöscht

Nein

Dieses Verzeichnis ist die Sitzung.

Abmelden innerhalb des Fensters

Nein

LinkedIn widerruft sie.

LinkedIn lässt das Cookie ablaufen

Nein

Siehe unten.

Wie viel Zeit LinkedIn Ihnen gibt. linkedin_session_info meldet das Ablaufdatum des li_at-Cookies und die verbleibenden Tage, live aus dem Cookie-Speicher des Browsers gelesen – Sie müssen also nie raten, und es ist eine Messung, kein eine Behauptung in einer README. Zur Kalibrierung: Die eigenen langlebigen Cookies von LinkedIn in diesem Profil (bcookie, bscookie) wurden mit einer 365-Tage-Gültigkeit ausgestellt. Der li_at-Wert ist derjenige, der für die Anmeldung maßgeblich ist, und nur eine echte Anmeldung kann ihn erzeugen.

Cookie-Werte sind Anmeldedaten: niemals protokolliert, niemals von diesem Server persistiert, niemals in einem Tool-Ergebnis. Nur der Name, das Vorhandensein und das Ablaufdatum.

Wenn sie abläuft, meldet jedes Lesetool genau das – {"error": "not_authenticated", "message": "..."} – und nennt linkedin_login_browser als den Weg zurück. Es gibt niemals stattdessen eine Liste zurück; eine leere Liste aus einer abgelaufenen Sitzung ist nicht von einer leeren Liste zu unterscheiden, die daher stammt, dass Sie wirklich keine haben.

Der Kaltstart und die Falle darin

li_at ist ein persistentes Cookie. JSESSIONID – das LinkedIn eigene Web-App in den csrf-token-Header kopiert und ohne das der Identitäts-Endpunkt eine authentifizierte Anfrage nicht beantwortet – ist ein Sitzungs-Cookie (is_persistent=0 im Cookie-Store dieses Profils). Bei jedem Browserstart enthält der Store also eine einwandfreie Anmeldung und kein csrf-Token.

Ein Server, der den Identitäts-Endpunkt direkt fragen würde, schickte eine Anfrage ohne Token, würde abgelehnt und man bekäme die Aufforderung, sich neu anzumelden, obwohl die Sitzung in Ordnung war. Bei einem kalten Store lädt check_auth also zuerst eine LinkedIn-Seite, wodurch LinkedIn das Cookie ausstellt, und fragt erst dann. Dieses Laden ist zugleich der bestätigende Lesevortrag und kostet so keine zusätzliche Anfrage.

Der Wiederherstellungspfad: das eigene Chrome verwenden

Nicht der Alltagspfad. Das persibstente Profil oben ist die Antwort; hier ist der Notfallplan für den Tag, an dem die Sitzung dieses Profils stirbt und eine erneute Anmeldung abgelehnt wird. Aktivieren Sie es mit LINKEDIN_CDP_ATTACH=1, und dieser Server startet nichts – er bindet sich über CDP an ein Chrome, das Sie gestartet haben.

Zwei Dinge bringen das still zum Scheitern, beide auf dieser Maschine gemessen:

  1. Ein über die Taskleiste geöffnetes Chrome hat keinen DevTools-Port. „Mein Browser ist geöffnet“ ist nicht genug; es muss mit --remote-debugging-port gestartet worden sein.

  2. Chromes Singleton-Eigenschaft frisst das Flag. Läuft bereits ein Chrome, reicht das Starten eines zweiten mit dem Flag die Argument an das erste weiter und beendet sich – kein Port, kein Fehler, Exit-Status null.

Beenden Sie also zuerst Chrome vollständig (Fenster und die Hintergrundinstanz), damit Ihr echtes Profil und damit Ihre echte LinkedIn-Sitzung erhalten bleibt:

"C:\Program Files\Google\Chrome\Application\chrome.exe" --remote-debugging-port=9224

oder geben Sie ihm ein eigenes Profil, das neben Ihrem laufenden Chrome funktioniert, aber an nichts angemeldet ist; in diesem Fenster melden Sie sich dann einmal bei LinkedIn an:

"C:\Program Files\Google\Chrome\Application\chrome.exe" --remote-debugging-port=9224 --user-data-dir="%LOCALAPPDATA%\linkedin-cdp"

Das Gelingen bestätigen Sie durch das Öffnen von http://127.0.0.1:9224/json/version – JSON bedeutet, der Port ist aktiv. Oder Sie rufen linkedin_cdp_status auf, das für Sie testet und das Verbindungskommando meldet, wenn nichts antwortet. Die Adresse ist 127.0.0.1 und nicht localhost: Chrome bindet den Port nur auf IPv4 an; der Name löst daher zuerst zu [::1] auf und ein Timeout vergeht (gemessen 2085 ms gegenüber 35 ms).

Port 9224, bewusst nicht den Port 9223 des benachbarten Naukri-Servers.

Im Anbindungsmodus übernimmt dieser Server keine Profilsperre (er hat kein Profil), arbeitet in einem eigenen Tab und beim Herunterfahren trennt er die Verbindung, ohne Ihren Browser zu schließen – Playwrights close() auf einer CDP-Verbindung verwirft nur den Client; das wurde an einem echten Chrome gemessen, bevor derart vorgenommen wurde. Die Nur-Lese-Allowlist ist in beiden Modis identisch.

Raten-Disziplin

  • Ein pauschales Mindestintervall der 3 Sekunden zwischen jedes Seiten laden, global erzwungen. Throttling, keine Tarnung: es wird bewusst nicht gejit't, um irgendetwas zu ähneln.

  • Wer eine Seite pro Tool-Aufruf lädt. Die einzige Ausnahme ist linkedin_my_profile(include_skills=True), die eine zweite Seite lädt und pages_loaded: 2 meldet.

  • Kein automatisches Umblättern. Fragen Sie die nächste Seite einer Suche ganz bewusst mit start=25 an. Jedes Listenergebnis trägt capped, page_had und limit, damit „25 Ergebnisse“ nie mit „25 Ergebnisse gibt es“ verwechselt wird.

  • Ein Aufruf nach dem anderen, serial im Prozess; ein Prozess nach, serialisiert durch eine prozessübergreifende Sperre auf das Chrome-Profil. Wenn zwei Prozesse auf einem Chromium-Benutzerdatenverzeichnis liegen, beschädigen sie es und die Sitzung ist weg – das hat einen benachbarten Server betroffen.

  • Das Fenster bleibt nicht dauerhaft geöffnet. Der Browser schließt nach 5 Minuten Inaktivität und gibt die Sperre frei.

Wenn etwas nicht gelesen werden kann

Der Server wirft eine Exception, statt eine leere Liste zu liefern. Eine leere Liste einer Seite, die nicht gerendert werden konnte, ist nicht von einer leeren Liste zu unterscheiden, die daher kommt, dass Sie wirklich nichts vorhanden sind, und diese beiden müssen niemals verwechselbar sein. Ein fehlgeschlagener Lesungsversuch kommt als {"error": "extraction_failed", "url": ..., "hint": ...} zurück, damit Sie selbst dieselbe Seite öffnen und sehen können, was sie gesehen hat.

Die einzige Ausnahme ist linkedin_search_jobs, dort ist null Ergebnisse eine echte Antwort; es liefert results: [] mit einem note-Feld.

Wie die Seiten gelesen werden

LinkedIn-Klassennamen werden generiert, und die GraphQL-Query-IDs rotieren mit jedem Deployment, beides sind daher brüchige Anker. Was sich nicht ändert, ist die Form eines Links: eine Person liegt hinter /in/<slug>, ein Job dahin /jobs/view/. Jede Listenfläche wird geerntet, indem diese Links gefunden und der Text der Karte um sie herum gelesen wird, dann mit puren Funktionen in shape.py geparst wird – genau aus diesem Grund ist das Parsing ohne Browser, Netzwerk und Konto testgetrieben.

Benachrichtigungen sind die einzige Fläche ohne zuverlässigen Link im Element selbst, daher ist der Anker die Struktur. Diese Seite ist am ehelichsten Pflege erforderlich und wirft bei den Fehlen eine Exception statt einer leeren Liste.

Layout

linkedin.py              entry point (stdio)
linkedin_server/
  config.py                  paths, timeouts, caps, the rate floor,
                             the two launch flags
  readonly.py                the allowlist, the scanners, the verb list,
                             the launch boundary
  profile_lock.py            cross-process lock on the Chrome profile
  browser.py                 persistent context, single-flight, idle close
  auth.py                    the login gate, session lifetime, cold start
  cdp_bridge.py              the recovery path: attach to a running Chrome
  dom.py                     the read-only harvesters and the control readers
  shape.py                   pure parsers and the result envelope
  server.py                  the seventeen tools
  errors.py
tests/                       1393 tests, no network, no account
  fixtures/                  frozen LinkedIn markup, scrubbed

Workstate

Gebaut und getestet: 1393 Tests, ohne Netzwerk und ohne Konto. Die meisten laufen ohne Browser; die fixture-basierten Module starten einen lokal headless Chromium, um die echten Read-Bibliotheken über eingefrorenes Rendering laufen zu lassen, das nichts außerhalb der Maschine erreicht.

Diese Zahlen sind die, die in dieser Datei am häufigsten falsch waren. Sie zeigte 986 über drei Releases, nachdem die Suite tausend überschritten waren, was für sich unschädlich ist und dieselbe attitude, vier Dokumente erklärten, der Server könne nicht schreiben. Sie werden nun bei jedem Release neu gemessen und nicht einfach fortgeschoben.

Erster Live-Lauf: 2026-08-21. Die Anmeldung war erfolgreich und die Sitzung blieb im Profil bestehen; daher ist das Merkmal jetzt auf dieser Maschine als ausreichend verifiziert, außerdem sind /voyager/api/me und die Lebensdauer von li_at (365 Tage) bestätigt. Danach wurde jedes Lotool genau einmal gegen das echte Konto ausgeführt. Vier der elf funktierten; der Untersuchungsansatz ist beschrieben in ../_audit/2026-08-21-linkedin-parse-fix.md, und zwar was festgestellt wurde.

linkedin_who_viewed_me lieferte Namen, die keine Namen waren. Jede Zeile trug die Seitenüberschrift, „Wer hat Ihr Profil angeschaut", verknüpft mit einem echten Personen-Profil-Link: vier Zeilen, ein wiederholter Name, und alle vier Links waren wirklich echt. Es wurde am selben Tag behoben: Die Zeilengrenze hängt nicht länger von einem Attribut ab, das LinkedIn erst nach Hydration einhängt; durch Datenschutz eingeschränkte Personen werden nicht mehr stumm verworfen (es waren sechs von zehn), und die Zeitstempel werden gelesen. Ein live verifiziert: 10 Zeilen, 10 verschandelte Namen, keinsstaff fehlt.

Zweiter Durchlauf, 2026-08-22. Die drei Surfacedaten, die der erste Durchlauf unfertig hinterließ, wurden repariert und live verifiziert. Alle vier Fehler hatten dieselbe Form: ein Parser, der an einem Markup hing, das LinkedIn nicht mehr ausgibt, oder an Markup, dessen Vorhandensein davon abhängt, wie weit die Seite gerendert wurde.

Tool

war

jetzt

linkedin_my_profile

Fehler: Kein Name lesbar

liest Name, Headline, Ort, „Über“ und Foto von einer Seite mit null h1. Ein Abschnitt ist jetzt der größte Vorfahre seiner Überschrift, die exakt EINE Überschrift enthält – dieselbe Regel, die auch der Zeilenlauf nutzt – daher identische Ausgabe vor und nach der Hydratisierung. Live verifiziert.

linkedin_saved_jobs, linkedin_my_applications

Fehler beim Weiterleitungs

last /jobs-tracker/?stage=saved und ?stage=applied. Beide Listen sind wirklich leer; ein leeres Ergebnis sagt das jetzt explizit, mit Of LinkedIins Tab-Zähler und dem "Dder wurden bis ein`t" empty-state Text. Eine Null, die die Seite nicht bestätigt, bleibt Fehler. Live verifiziert.

linkedinnotifications`

Zeilen, Rauschen

darlet Screen-Reader-Text wird nach Anzahl abgezogen, statt nach Position, und when kommt aus dem eigenen Zeit-Element der Karte. Jede Zeilstücke unread so wie beim Lesen. Verifiziert gegen einen eingefrorenen Schnappschuss der LiveSeite.

skills, innerhalb my_profile

lieiterte All, Industry Knowledge, Tools & Technologies

liefert die echte Liste – also 20 Fähigkeiten im Live-Konto –, übergelagert auf den einzigen Pro-Fähigkeit-Anker, den die Seite zu redet.

Eine Sache macht der Profil-Leser nicht: Erfahrung, Ausbildung und Skills stehen überhaupt nicht auf der Profilseite. Sie werden nachgeladen, LinkedIn lädt sie erst durch Scrollen, und dieser Server scrollt nicht. Sie stehen als UNKNOWN, nie als 0, und details_urls liefert Ihnen die Seite für jeden Eintrag.

Dritter Durchlauf, 2026-08-22. linkedin_search_jobs war das letzte defekte Werkzeug. Bei einer Zeile für ein verifiziertenes Arbeitgebers fügt LinkedIn eine Zeile für Screenreaders hinzu, die im Fragment „ mit der Vorverifikation“ liest; Positional gelesen wurde das zu company und drückte der echte Arbeitgeber in location – 5 von 14 Zeilen bei zwei Live-Suchen betroffen.

Die Korrektur ist keine Regel, die diese Zeichenfolge betrifft. Felder werden nicht mehr als „Zeile 1, Zeile 2, Zeile 3“ gelesen, denn jede Zeile, die LinkedIn einfügt, verschiebt alle danach liegenden Felder; dieselben zwei Seiten enthielten außerdem „Gesponsert“, „Jetzt bewerben“, „Angesehen“, „Prüft aktiv Bewerbungen“, einen Gehalts-Chip und eine Alumni-Zeile. Jedes Feld ist jetzt an dem verankert, was es IDENTIFIZIERT: der Titel an dem Text des Links, der die Zeile zur Job-Zeile macht, wobei die eigenen Screenreader-Kopien der Seite zahlenmäßig abgezogen werden; das Unternehmen an dem zugänglichen Namen, den LinkedIn dem Logo des Arbeitgebers gibt, dass ein Bild ist und sich durch eine eingefügte Zeile nicht verschieben lässt; der Standort an der Metadatenliste im Entity-Lockup, wobei das Lockup ohne jeder Klassennamen als die kleinste Vorfahre des Links gefunden wird, der ebenfalls dieses Logo enthält. Eine Oberfläche, die keines dieser Anker enthält – der Job-Tracker bietet keinen –, fällt wie zuvor darauf zurück, die Zeilen der Reihenfolge nach zu lesen. Live mit derselben Suchanfrage verifiziert: 7 von 7 Zeilen stimmen mit den LinkedIn-eigenen artdeco-entity-lockup-Elementen überein, die die Korrektur bewusst nicht verwendet, und 3 dieser 7 trugen die Verifizierungs-Dekoration. Die Tests injizieren an jeder Position in jeder eingefrorenen Zeile eine Dekoration, die LinkedIn nicht ausgeliefert hat, und verlangen, dass sich die Antwort nicht bewegt; die kontrolle zeigt, dass dieselbe Injektion die Feldernicht, sobald die Anker entfernt werden.

Install Server
F
license - not found
A
quality
B
maintenance

Maintenance

Maintainers
Response time
Release cycle
Releases (12mo)
Commit activity

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Related MCP Servers

View all related MCP servers

Related MCP Connectors

View all MCP Connectors

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/Sundeepg98/linkedin-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server