# @hoyasumii/linkedin > An unofficial TypeScript SDK and MCP server to create, edit and delete your own LinkedIn posts. This file contains all documentation content in a single document following the llmstxt.org standard. ## Getting started `@hoyasumii/linkedin` lets you, or an AI agent working for you, **create, edit and delete your own LinkedIn posts**: text, images, a video, a document or an article link. - **MCP server** (`@hoyasumii/linkedin/mcp`, bin `linkedin-mcp`): six tools over stdio, for Claude Code, Codex, OpenCode or any MCP client. Start at [MCP server](./mcp/overview.md). - **SDK**: `createLinkedInClient`, typed methods over LinkedIn's Posts, Images, Videos and Documents APIs. It is generated by [orval](https://orval.dev) from an OpenAPI spec written for this package. Start at [SDK](./sdk/overview.md). - **CLI** (`linkedin`): it posts nothing itself. `linkedin mcp config --web` signs you in to LinkedIn, and `linkedin mcp install` registers the server in your AI clients. Start at [CLI](./cli/overview.md). This is an independent, **unofficial** client, MIT licensed. It is not affiliated with or endorsed by LinkedIn. ### What LinkedIn allows LinkedIn's API is open to any member's app for **writing** posts, but not for **reading** them: | You want to | Permission | Available to your own app? | | --------------------------------------------- | ----------------- | ---------------------------------------------------------- | | Create, edit (the text) and delete your posts | `w_member_social` | Yes, from the "Share on LinkedIn" product, granted at once | | Know who signed in | `openid profile` | Yes, from "Sign In with LinkedIn using OpenID Connect" | | Read your posts back | `r_member_social` | No: LinkedIn has closed it to new apps | So this package keeps a **local list of the posts it created or edited**, and that is what its "read" tools show. Posts you make on linkedin.com are not listed. You can still edit or delete them by their URN or URL. See [Reading your posts](./mcp/tools.md#reading-your-posts). ### Installation Requires Node.js 20 or later, and a LinkedIn app of your own, which takes five minutes to create: [Creating the LinkedIn app](./linkedin-app.md). ```bash npm i -g @hoyasumii/linkedin # or: pnpm add -g @hoyasumii/linkedin linkedin mcp config --web # paste the app's Client ID and Secret, then sign in to LinkedIn linkedin mcp install # registers linkedin-mcp (stdio) in Claude Code / Codex / OpenCode ``` Then ask your agent: _"Post on LinkedIn that I just released v1.0 of my project, with screenshot.png."_ As a library, `npm install @hoyasumii/linkedin`: ```ts import { createLinkedInClient } from "@hoyasumii/linkedin"; const linkedin = createLinkedInClient({ accessToken: process.env.LINKEDIN_ACCESS_TOKEN! }); const { urn, url } = await linkedin.createPost({ text: "Hello from the API (yes, with parentheses)!" }); await linkedin.editPost(urn, { text: "Hello from the API, edited." }); await linkedin.deletePost(urn); ``` `linkedin docs` opens this site. --- ## Creating the LinkedIn app LinkedIn only issues tokens to an app, and an app's Client Secret cannot ship inside an npm package. So you create your own app, once. It is free and needs no review. 1. **A LinkedIn Page.** LinkedIn attaches every app to a Company Page. Any page you administer works. If you have none, [create one](https://www.linkedin.com/company/setup/new/): it can stay empty, and nothing is posted to it. 2. **The app.** At [linkedin.com/developers/apps/new](https://www.linkedin.com/developers/apps/new), give it a name (say, "My MCP"), pick the page, upload any logo and accept the terms. 3. **Its products.** In the app's **Products** tab, request: - **Share on LinkedIn**, which grants `w_member_social` to create, edit and delete your posts; - **Sign In with LinkedIn using OpenID Connect**, which grants `openid profile` to know who signed in. Both are granted at once. 4. **The redirect URL.** In the **Auth** tab, under _OAuth 2.0 settings_, add this **Authorized redirect URL**: ```text http://localhost:3769/callback ``` It is where LinkedIn sends you back after you sign in, to the page `linkedin mcp config` serves. Another port works too (`linkedin mcp config --redirect-port `), as long as both say the same. 5. **The credentials.** In the same **Auth** tab, copy the **Client ID** and the **Primary Client Secret**. Then run `linkedin mcp config --web`: paste both, press _Save and sign in with LinkedIn_, and allow the app on LinkedIn's page. The Client Secret and the token are saved on your computer only, readable by you alone (see [Configuration](./mcp/configuration.md)). ### Renewing the sign-in A LinkedIn member token lasts **60 days**, and LinkedIn gives ordinary apps no refresh token. When it runs out, the tools answer "not signed in" and your agent tells you to run `linkedin mcp config --web` again. That takes a click, since the app is already saved. `linkedin mcp status` shows how many days are left. If LinkedIn does give your app a refresh token (an option it enables for some partners), the server refreshes the token by itself. ### Limits - 150 posts a day per member, and 100,000 calls a day per app. - The token can post only as you, never as a page or another member. - To revoke the app's access, remove it at [linkedin.com/psettings/permitted-services](https://www.linkedin.com/psettings/permitted-services). --- ## Configuration `linkedin mcp config` saves three files in a per-user directory, each readable by you alone (mode `0600`; on Windows, the folder's permissions): | File | Holds | | ------------------ | --------------------------------------------------------------------------------------------------------------------------- | | `.env` | The app: `LINKEDIN_CLIENT_ID`, `LINKEDIN_CLIENT_SECRET`, and optionally `LINKEDIN_REDIRECT_PORT` and `LINKEDIN_API_VERSION` | | `credentials.json` | The sign-in: access token, expiry, person URN, name, scopes | | `posts.json` | The [local list of posts](./tools.md#reading-your-posts) | The directory is `~/.config/linkedin` on Linux (`$XDG_CONFIG_HOME/linkedin`), `~/Library/Application Support/linkedin` on macOS and `%APPDATA%\linkedin` on Windows. `LINKEDIN_CONFIG` points at another `.env`; the other two files sit beside it. ### Environment Read by `linkedin-mcp` at launch, over the saved values: | Variable | Effect | | ---------------------------------------------- | -------------------------------------------------------------- | | `LINKEDIN_CONFIG` | Another `.env` (and so another sign-in and post list) | | `LINKEDIN_ACCESS_TOKEN` | Use this token instead of `credentials.json`; never saved | | `LINKEDIN_PERSON_URN` | With a token: the author, sparing the `userinfo` call | | `LINKEDIN_API_VERSION` | The `LinkedIn-Version` header, `YYYYMM` (default `202609`) | | `LINKEDIN_CLIENT_ID`, `LINKEDIN_CLIENT_SECRET` | The app, to refresh a token when LinkedIn gave a refresh token | --- ## Overview ## MCP server `linkedin-mcp` is an MCP server that posts on **your** LinkedIn profile. An MCP client (Claude Code, Codex, OpenCode, Claude Desktop…) launches it and speaks to it over stdio. | Tool | What it does | | ---------------------- | --------------------------------------------------------------------------- | | `linkedin_whoami` | Who is signed in, and when the sign-in expires | | `linkedin_create_post` | Publishes a post: text, plus images, a video, a document or an article link | | `linkedin_edit_post` | Replaces a post's text | | `linkedin_delete_post` | Deletes a post (needs `confirm: true`) | | `linkedin_list_posts` | The posts made or edited through this server, newest first | | `linkedin_get_post` | One of those posts | Details on each: [Tools](./tools.md). The server holds no secret in the client's config: it reads the sign-in `linkedin mcp config` saved (`credentials.json`) on every call, so signing in again takes effect without restarting the client. To set it up: [Setup](./setup.md). To embed it in your own program: [Programmatic use](./programmatic.md). --- ## Programmatic use `@hoyasumii/linkedin/mcp` exports the server's pieces: ```ts import { buildLinkedInMcpServer, configFilePath, connectionFor, readEnvFile, resolveMcpConfig, serveLinkedInMcpStdio, } from "@hoyasumii/linkedin/mcp"; const configFile = configFilePath(); const config = resolveMcpConfig({ env: process.env, file: readEnvFile(configFile), configFile }); const connection = connectionFor(config); // { client, registry, credentialsFile } // Over stdio, as linkedin-mcp does: const running = await serveLinkedInMcpStdio(connection); await running.closed; // Or the bare McpServer, for any transport: const server = buildLinkedInMcpServer(connection); ``` `connectionFor` builds the SDK client from the saved sign-in (or `LINKEDIN_ACCESS_TOKEN`) and the post list beside the configuration. A connection of your own works too: `{ client: createLinkedInClient({ … }), registry: new PostRegistry("posts.json") }`. --- ## Setup ### The quick way With [your LinkedIn app](../linkedin-app.md) created: ```bash npx -p @hoyasumii/linkedin linkedin mcp config --web # paste the Client ID and Secret, sign in on LinkedIn npx -p @hoyasumii/linkedin linkedin mcp install # register linkedin-mcp in the clients found ``` Or install the package globally (`npm i -g @hoyasumii/linkedin`) and drop the `npx -p …`. `install` shows a checklist of the clients it found on your PATH: Claude Code, Codex and OpenCode. Tick the ones you want, and it registers the server through each client's own CLI, under the name `linkedin`. It launches `node /dist/mcp/cli.js` by absolute path, so it does not depend on the PATH the client hands its servers. See [`linkedin mcp install`](../cli/mcp-commands.md#linkedin-mcp-install) for the flags. ### By hand Any client that runs stdio servers can start it. It needs nothing but the saved sign-in: ```json { "mcpServers": { "linkedin": { "command": "npx", "args": ["-y", "-p", "@hoyasumii/linkedin", "linkedin-mcp"] } } } ``` In Claude Code: ```bash claude mcp add linkedin -s user -- npx -y -p @hoyasumii/linkedin linkedin-mcp ``` To use a token from elsewhere instead of the saved sign-in (a CI job, a second account), pass it in the environment: `LINKEDIN_ACCESS_TOKEN`, plus `LINKEDIN_PERSON_URN` to skip the `userinfo` call. See [Configuration](./configuration.md). ### Check it Ask your agent _"Who am I on LinkedIn?"_: it calls `linkedin_whoami`. From the terminal, `linkedin mcp status` shows the same. --- ## Tools ### linkedin_create_post Publishes a post on the signed-in member's profile, right away. | Input | Type | Notes | | ----------------- | ------------------------------------------- | --------------------------------------------------- | | `text` | string | Plain text; reserved characters are escaped for you | | `visibility` | `PUBLIC` \| `CONNECTIONS` | Default `PUBLIC` | | `images` | `{ path, alt_text? }[]` | 1, or 2 to 20 for a gallery | | `video` | `{ path, title? }` | MP4 | | `document` | `{ path, title? }` | PDF, PPT(X), DOC(X), shown as a carousel | | `article` | `{ url, title?, description?, thumbnail? }` | A link card; `thumbnail` is a local image | | `reshare_of` | string | A post URN or URL to reshare | | `disable_reshare` | boolean | | | `raw_text` | boolean | The text is already in little format (for mentions) | | `hashtags` | boolean | Default `true`: `#word` stays a hashtag | One kind of media at most. Paths are local to the machine the server runs on. It answers the post's `urn`, `url` and what was recorded in the local list. The server's instructions ask the agent to publish only what you asked for, and to show you text it wrote before posting it. ### linkedin_edit_post `post` (a URN or URL) and the new `text` (with `raw_text` and `hashtags` as above). Only the text changes: images, video, documents and links cannot be swapped, so delete the post and post again for that. ### linkedin_delete_post `post` and `confirm`. The server refuses unless `confirm` is `true`, and its description tells the agent to confirm the post with you first. Deleting is permanent. ### linkedin_whoami The member's name and person URN, when the sign-in expires and how many days are left. ### linkedin_list_posts and linkedin_get_post `linkedin_list_posts` takes `query` (text to look for), `limit` (default 20) and `include_deleted`. `linkedin_get_post` takes `post`. Both read the local list described below and never call LinkedIn. ### Reading your posts LinkedIn lets any app **write** a member's posts (`w_member_social`), but reading them back needs `r_member_social`, which LinkedIn has closed to new apps. Even fetching a single post by its URN needs it. Its self-serve alternative, the Member Data Portability API, is open only to members in the EU/EEA and Switzerland. So the server remembers what it does. `posts.json`, beside the saved configuration, holds every post it created, edited or deleted: URN, URL, text as last written, visibility, media and dates. That is what `linkedin_list_posts` and `linkedin_get_post` show. - A post you made on linkedin.com is not in the list. To edit or delete it, give the agent its URN or URL (**⋯** → **Embed this post** shows the URN). Once edited here, it joins the list. - An edit made on linkedin.com is not seen: the list keeps the last text written through the server. ### Errors Tool errors come back as text the agent can act on: - _Not signed in to LinkedIn …_ or _The LinkedIn sign-in expired …_: the sign-in is missing or expired, so run `linkedin mcp config --web`; - _LinkedIn answered 403 (the token lacks a permission …)_: the app is missing a product, or the post is not yours; - _LinkedIn answered 429_: the daily limit was reached; - file and text problems (format, size, length) are refused before anything is sent. --- ## Errors | Class | When | | ---------------------- | -------------------------------------------------------------------------------------------------------------- | | `LinkedInApiError` | LinkedIn answered a non-2xx status: `status`, `code` (`ACCESS_DENIED`…), `serviceErrorCode`, `message`, `body` | | `LinkedInAuthError` | No token, an expired one, or a 401: sign in again with `linkedin mcp config --web` | | `LinkedInConfigError` | Input refused before sending: a bad file, text too long, two kinds of media, a bad URN | | `LinkedInMediaError` | LinkedIn could not process a video or document, or did not finish in time | | `LinkedInTimeoutError` | No answer within `timeoutMs` | ```ts import { LinkedInApiError, LinkedInAuthError } from "@hoyasumii/linkedin"; try { await linkedin.deletePost(urn); } catch (error) { if (error instanceof LinkedInAuthError) console.error("Sign in again: linkedin mcp config --web"); else if (error instanceof LinkedInApiError && error.status === 404) console.error("No such post"); else throw error; } ``` The common statuses: - **403**: the token lacks a permission (the app is missing a product), or the post is not yours; - **404**: the post does not exist, or was deleted; - **429**: the daily limit (150 posts per member) was reached; - **426** or a version message: the `LinkedIn-Version` was retired, so set a newer `apiVersion`. `redact(value, secrets)` masks secrets in anything you log. Every error the SDK builds is already masked. --- ## Media `createPost` uploads the files it is given. The upload methods are also exposed on their own: ```ts const image = await linkedin.uploadImage("shot.png"); // urn:li:image:… const video = await linkedin.uploadVideo("demo.mp4"); // urn:li:video:…, once processed const document = await linkedin.uploadDocument("deck.pdf"); // urn:li:document:…, once processed ``` | Kind | Formats | Size | Notes | | -------- | -------------------------------- | ------------------------------ | ----------------------------------- | | Image | JPG, PNG, GIF (up to 250 frames) | under 36,152,320 pixels | 1 per post, or 2 to 20 as a gallery | | Video | MP4 | 75 KB to 500 MB, 3 s to 30 min | uploaded in 4 MB parts | | Document | PDF, PPT, PPTX, DOC, DOCX | up to 100 MB and 300 pages | shown as a swipeable carousel | The extension and the size are checked before anything is sent (`checkMedia`, `MEDIA_RULES`). Each upload asks LinkedIn for an upload URL (`initializeUpload`), then PUTs the bytes there. A video is uploaded in the parts LinkedIn lists, and finalized with each part's `ETag`. Videos and documents are then processed by LinkedIn. The SDK polls their status until they are `AVAILABLE`, every 2 seconds for up to 10 minutes (`poll: { intervalMs, timeoutMs }`). A token with only `w_member_social` may be refused that status read. The post is then retried while LinkedIn says the media is still processing. A failed processing throws `LinkedInMediaError` with LinkedIn's reason. --- ## Overview(Sdk) ## SDK ```ts import { createLinkedInClient } from "@hoyasumii/linkedin"; const linkedin = createLinkedInClient({ accessToken: process.env.LINKEDIN_ACCESS_TOKEN!, // or () => string | Promise, called per request author: "urn:li:person:Ab12Cd34", // optional: read once from OpenID Connect userinfo otherwise apiVersion: "202609", // optional: the LinkedIn-Version header (YYYYMM) timeoutMs: 120_000, // optional: per request; 0 turns it off }); ``` | Method | What it does | | ------------------------------------------------ | ------------------------------------------------------------------------- | | `createPost(params)` | Publishes a post; answers `{ urn, url, author }`. See [Posts](./posts.md) | | `editPost(post, { text })` | Replaces a post's text; `post` is a URN or a URL holding one | | `deletePost(post)` | Deletes a post (idempotent) | | `uploadImage` / `uploadVideo` / `uploadDocument` | Uploads media on its own; answers its URN. See [Media](./media.md) | | `me()` | The signed-in member (OpenID Connect `userinfo`) | | `author()` | The person URN posts are made as | Every method takes a last `{ signal }` argument to cancel it. ### The token The SDK does not sign in by itself: give it a member access token with `w_member_social` (and `openid profile` when you leave `author` out). Three ways to get one: - `linkedin mcp config --web` saves one; read it with `readCredentials(credentialsPath(configFilePath()))`, or use `credentialSource(file)`, which re-reads the file and refreshes it when it can; - LinkedIn's [OAuth token generator](https://www.linkedin.com/developers/tools/oauth/token-generator), for a quick test; - your own OAuth flow, with the helpers `authorizationUrl`, `exchangeCode` and `credentialsFrom`. ```ts import { createLinkedInClient, credentialSource, credentialsPath } from "@hoyasumii/linkedin"; import { configFilePath } from "@hoyasumii/linkedin/mcp"; const credentials = credentialSource(credentialsPath(configFilePath())); const linkedin = createLinkedInClient({ accessToken: async () => (await credentials()).accessToken, author: async () => (await credentials()).personUrn, }); ``` ### What it sends Every call to `/rest/…` carries `Authorization: Bearer …`, `LinkedIn-Version` and `X-Restli-Protocol-Version: 2.0.0`. URNs in paths are percent-encoded. A create answers `201` with the new URN in the `x-restli-id` header, which the SDK returns as `urn`. The token is sent only to LinkedIn's own hosts (upload URLs live on `www.linkedin.com`) and never appears in an error. LinkedIn keeps each `LinkedIn-Version` for about a year. When the default one is retired, set a newer one with `apiVersion` (or `LINKEDIN_API_VERSION`), or update the package. ### The local post list `PostRegistry` is the list of posts the MCP server made (`posts.json`). The SDK client does not write to it. See [Reading your posts](../mcp/tools.md#reading-your-posts). --- ## Posts ### Create ```ts const { urn, url } = await linkedin.createPost({ text: "Shipped v1.0 (finally) #release", visibility: "PUBLIC", // or "CONNECTIONS": 1st-degree connections only }); ``` A post carries **one** kind of media at most: ```ts await linkedin.createPost({ text: "One image", images: [{ file: "shot.png", altText: "The new dashboard" }] }); await linkedin.createPost({ text: "A gallery", images: ["a.jpg", "b.jpg", "c.png"] }); // 2 to 20 await linkedin.createPost({ text: "A video", video: { file: "demo.mp4", title: "Demo" } }); await linkedin.createPost({ text: "Slides", document: { file: "deck.pdf", title: "Our roadmap" } }); await linkedin.createPost({ text: "Worth reading", article: { url: "https://example.com/post", title: "The post", description: "…", thumbnail: "cover.png" }, }); await linkedin.createPost({ text: "Agreed!", reshareOf: "https://www.linkedin.com/feed/update/urn:li:share:7…/" }); ``` Media is given as a local path, or as `{ data: Uint8Array, filename, contentType }`. See [Media](./media.md) for the formats and limits. `disableReshare: true` stops others from resharing the post. LinkedIn does not fetch an article's page: the card shows only the `title`, `description` and `thumbnail` you set. A bare URL in the text gets LinkedIn's usual link preview instead. ### The text A post's text is in LinkedIn's _little_ format. Its reserved characters, `\ | { } @ [ ] ( ) < > # * _ ~`, must be escaped even where they form nothing. An unescaped `(` can make LinkedIn cut the post short without any error. The SDK escapes plain text for you: - line breaks and emoji are kept; - `#word` stays a hashtag (`hashtags: false` makes every `#` plain); a `#` that starts no word (`C#`) is escaped; - the limit is 3,000 characters **after** escaping, checked before sending. To mention someone, write the little format yourself and pass `rawText: true`: ```ts await linkedin.createPost({ text: "Thanks @[Ada Lovelace](urn:li:person:Ab12Cd34)\\!", rawText: true }); ``` `toLittle(text)` and `fromLittle(little)` convert by hand. ### Edit ```ts await linkedin.editPost(urn, { text: "Shipped v1.0.1 (a hotfix)" }); ``` Only the text can change: LinkedIn does not let a post's images, video, document or link be swapped. For that, delete the post and post again. The post shows as edited. ### Delete ```ts await linkedin.deletePost(urn); // or the post's URL ``` Deleting is permanent and takes the comments and reactions with it. Deleting a post again succeeds. ### Which posts `editPost` and `deletePost` take a post URN (`urn:li:share:…` or `urn:li:ugcPost:…`) or a URL holding one, such as `https://www.linkedin.com/feed/update/urn:li:share:…/`. The link from _Copy link to post_ names the feed _activity_ (`…-activity-7…`), which the API does not accept. On linkedin.com, open the post's **⋯** menu → **Embed this post** instead: the embed code holds the post's URN. `parsePostRef` accepts every form and explains the activity case. --- ## linkedin mcp ### linkedin mcp config Saves your [LinkedIn app](../linkedin-app.md) and signs you in. ```bash linkedin mcp config --web # everything in a local web page (recommended) linkedin mcp config # asks for the Client ID and Secret here, then opens the sign-in linkedin mcp config --client-id … --client-secret … # no questions (scripts) ``` With `--web` it serves a page on `http://localhost:3769` that lists the app setup steps, with the redirect URL to copy. You paste the Client ID and the Client Secret there, then press _Save and sign in with LinkedIn_. LinkedIn asks you to allow the app and sends you back to `http://localhost:3769/callback`. The page then saves the sign-in and says who you are and until when. The command waits up to 10 minutes. | Flag | Effect | | -------------------------------- | -------------------------------------------------------------------------- | | `--web` | Do it in the web page | | `--client-id`, `--client-secret` | Save these without asking (the secret stays in your shell history) | | `--redirect-port ` | Listen there instead of 3769 (`''` resets); the app must list the same URL | | `--api-version ` | The `LinkedIn-Version` to send (`''` resets) | | `--no-login` | Only save the settings | | `--no-open` | Print the link instead of opening the browser | | `--config ` | Another `.env` (env `LINKEDIN_CONFIG`) | A blank secret keeps the saved one. Running it again later, when the 60-day token expires, takes a single click. The page is guarded like the token it saves. Every URL of ours carries a random token, the `Host` header is checked, and LinkedIn's redirect must carry a one-time `state` issued by that page. ### linkedin mcp status Who is signed in, until when, the app's Client ID and the redirect URL. It exits `3` when nobody is signed in or the sign-in expired. ### linkedin mcp logout Deletes `credentials.json`. The app settings stay. To also revoke the app's access on LinkedIn's side, remove it at [linkedin.com/psettings/permitted-services](https://www.linkedin.com/psettings/permitted-services). ### linkedin mcp install Registers `linkedin-mcp` (stdio) in Claude Code, Codex and OpenCode, as `linkedin`. It needs a sign-in. Without `--client` it shows a checklist of the clients found on the PATH. | Flag | Effect | | ----------------------- | ------------------------------------------------------------------------ | | `--client claude,codex` | Skip the checklist (`claude`, `codex`, `opencode`; `…@windows` from WSL) | | `--force` | With `--client`: replace an existing `linkedin` entry | | `--dry-run` | Print the commands instead of running them | | `--config ` | Register with another `.env` (passed as `LINKEDIN_CONFIG`) | Restart the client, or reconnect its MCP servers, to load it. ### linkedin mcp uninstall Removes the `linkedin` entry from the clients picked (or `--client`), with `--dry-run` to preview. It runs without a sign-in, so a client can be cleaned up afterwards. OpenCode has no `mcp remove`, so its config file is edited in place, keeping its comments. --- ## Overview(Cli) ## CLI The `linkedin` command does not post anything. Posting is the job of the [MCP server](../mcp/overview.md), or of the [SDK](../sdk/overview.md) in your own code. The CLI does the setup: ```bash linkedin mcp config --web # save your LinkedIn app and sign in, in a local web page linkedin mcp install # register linkedin-mcp in Claude Code, Codex or OpenCode linkedin mcp status # who is signed in, and for how many more days linkedin mcp logout # forget the sign-in on this computer linkedin mcp uninstall # remove the server from your clients linkedin docs # open this site ``` Every command: [`linkedin mcp`](./mcp-commands.md). On Windows and from WSL: [Windows and WSL](./windows.md). --- ## Windows and WSL The package runs on Windows 10/11 with Node.js 20 or later, from PowerShell or cmd. - The settings, the sign-in and the post list live in `%APPDATA%\linkedin\`, protected by that folder's per-user permissions (file modes mean nothing on Windows). - The client CLIs are run through `cross-spawn`, so Windows `.cmd` shims work. - Files are written through a rename that retries, because antivirus and editors hold files open. ### From WSL When the package is installed inside WSL, `linkedin mcp install` and `uninstall` also list the clients installed on the Windows side, as `Claude Code (Windows)` and so on (`--client claude@windows`). They start the server with `wsl.exe -d -e node …/dist/mcp/cli.js`, so it keeps reading the sign-in saved inside WSL. The first call after WSL has been idle waits for the distro to start (a second or two). The Windows side is reached through `powershell.exe`, taken from the PATH or, with `appendWindowsPath = false`, from `/mnt/c/Windows/System32/WindowsPowerShell/v1.0/`. When it cannot be reached, `--client claude@windows` says which step failed. `linkedin mcp config --web` inside WSL serves the page on `localhost:3769` inside WSL, which the Windows browser reaches through WSL's localhost forwarding (on by default). --- ## Contributing The repository is [Hoyasumii/linkedin](https://github.com/Hoyasumii/linkedin), managed with pnpm (Node.js 20 or later). ```bash pnpm install # dependencies, plus the git hooks (husky) pnpm build # tsc → dist/ (CommonJS + .d.ts) pnpm test:unit # jest, with a fake LinkedIn on node:http (no network) pnpm test:live # against the real LinkedIn: posts, edits and deletes (.env.test, from env.example) pnpm check:types # tsc --noEmit over src and tests pnpm check:lint # oxlint (`pnpm fix:lint` fixes what it can) pnpm check:format # oxfmt, 120 columns (`pnpm fix:format` rewrites) pnpm check:knip # unused files, exports and dependencies ``` Every script is cross-platform: no `rm`, `$VAR` or `VAR=1 cmd`. `.gitattributes` keeps LF, with `.cmd`/`.vbs` in CRLF. The checks run locally through git hooks. `pre-commit` runs `check:lint` and `check:format`, `commit-msg` runs commitlint with the conventional config (`feat: …`, `fix(mcp): …`), and `pre-push` runs `check:types`, `check:knip` and `test:unit`. Every push to `main` runs the Continuous Delivery workflow (`.github/workflows/cd.yml`). It runs the same checks and the build, then: - publishes `package.json`'s version to npm when that version is not on the registry yet (through Trusted Publishing, with provenance), tags it `v` and opens a GitHub release; - builds the site and deploys it to the `gh-pages` branch when the push touches `website/` or `src/` (a manual run of the workflow always deploys it). To release, bump `version` in `package.json` and merge to `main`. ### Generated code ```bash pnpm codegen # spec/openapi.yml → src/generated/ (orval) ``` `spec/openapi.yml` is the single source: an OpenAPI 3.1 description of the part of LinkedIn's API this package uses (Posts, Images, Videos, Documents and OpenID Connect `userinfo`), written for this package from LinkedIn's public documentation on Microsoft Learn. `orval.config.ts` generates the `fetch` functions (through `src/transport.ts`), the model types and the zod schemas. Never edit `src/generated/` by hand: change the spec and run `pnpm codegen`. To follow a change in the API: update the spec, run `pnpm codegen`, adjust `src/client.ts` and the tools, and check it against LinkedIn with `pnpm test:live`. When LinkedIn retires a version, bump `DEFAULT_API_VERSION` in `src/client.ts`. ### This site The site is a Docusaurus workspace package in `website/`, in English and Portuguese (Brazil). ```bash pnpm docs:dev # preview (append `--locale pt-BR` for the translation) pnpm docs:build # build every locale into website/build/ pnpm docs:serve # serve the build (search only works on a build) GIT_USER= pnpm docs:deploy # build and push to the gh-pages branch ``` - The guides are plain Markdown in `website/docs/`, mirrored page for page in `website/i18n/pt-BR/docusaurus-plugin-content-docs/current/`: change both together. - The [API reference](pathname://../docs/api) is generated from `src/index.ts` and `src/mcp/index.ts` by TypeDoc on every English build. - `llms.txt` and `llms-full.txt` are generated at the site's root from the English guides on every build.