Skip to main content
Glama
IzikStar

linkedin-agent-mcp

by IzikStar

linkedin-agent-mcp

WARNING

Experimental project. Use entirely at your own risk.

This is a personal engineering experiment in agent safety design (typed tools, out-of-band human confirmation, failure-honest errors). It is published to show the code, not as a product or a recommendation to automate LinkedIn.

  • How I have used it: only on my own account, a handful of times, with me watching: read-only checks (search, a job page, my own profile, my saved jobs) and a single save/unsave of one posting to verify the confirmation flow. I have never used it to scrape data, send messages or connection requests, apply to jobs, or run anything in bulk or unattended.

  • LinkedIn's terms: LinkedIn's User Agreement (§8.2) prohibits bots and other automated access, and this project drives a real browser on a real account. Running it may get your account restricted or banned.

  • Your responsibility: anyone who runs this code does so at their own risk and is solely responsible for complying with LinkedIn's terms and applicable law. The software is provided "as is", without warranty of any kind (see LICENSE), and the author accepts no liability for how it is used.

A personal, human-in-the-loop LinkedIn job-search agent, exposed as an MCP server so an MCP client such as Claude Code can search jobs, read postings and your profile, track what you have seen, and prepare actions, while every consequential step on LinkedIn requires your explicit confirmation, given out-of-band in your own terminal.

It is deliberately not a mass-automation or scraping system: one browser page at a time, small capped result sets, spaced navigation, an hourly cap on external actions, and no evasion techniques of any kind.

At a glance

  • What it is: a TypeScript (Node 24) MCP server with 20 typed tools that let an AI agent search jobs, read postings and your profile, track what you have seen, and prepare actions on your own LinkedIn account.

  • Ports and adapters: the MCP layer is thin (zod schemas + error mapping); services depend on a LinkedInClient interface and repository interfaces, so Playwright lives in exactly two files and SQLite sits behind swappable repositories.

  • READ / PREPARE / EXECUTE, enforced structurally: each tool group is handed only the ports it may use, so a READ or PREPARE tool cannot reach the executor, not by prompt instructions but by construction.

  • Out-of-band confirmation: there is no confirm tool. Every external action needs a human to run npm run confirm in a real terminal and type a code; the payload is SHA-256 bound to that approval, single-use (database compare-and-swap), and time-limited.

  • Failure-honest: ambiguous writes become OUTCOME_UNKNOWN and are never auto-retried; LinkedIn markup changes surface as a typed LINKEDIN_CHANGED, never as made-up data; the audit log is append-only via database triggers.

  • Tested: 199 vitest tests (parsers on captured/modelled fixtures, state machines, the whole confirmation workflow including tampering and races, and a spawned stdio server through the real MCP SDK client), plus an opt-in read-only E2E suite run against a real account.

  • Documented decisions: 13 ADRs in docs/decisions/.

Status (2026-09-23): read-only flows (auth, search, job detail, own profile, saved jobs) are verified live on a real signed-in account, and so is the one registered write, Save/Unsave, run once end to end through the terminal confirmation with the owner present. Easy Apply, messaging, connections and profile writes are not implemented yet. Details in Verification status and CLAUDE.md.

Read this first: ToS and status. LinkedIn's User Agreement (§8.2) prohibits bots and other automated access. There is no official LinkedIn API for job search, saved jobs or applications for job seekers (see LinkedIn API limitations). This project drives a real browser on your own account. That is a risk to your account that only you can accept, and the browser will not start until you have (npm run login). Also see Verification status: parts are verified live, parts are only unit-tested.

Contents

Architecture · Why MCP · Stack · READ / PREPARE / EXECUTE · Security model · Install · Authentication · Configuration · Claude Code · Tools · Confirmation · Application workflow · Persistence · Testing · Verification status · Limitations · Roadmap · Responsible use · Decision log

Related MCP server: linkedin-mcp-server

Architecture

flowchart TB
    CC["Claude Code / MCP client"] -->|"stdio JSON-RPC"| MCP["MCP server (thin: schemas, kind labels, error mapping)"]

    subgraph tools["Tool groups get only the ports they may use"]
        R["READ tools"]
        P["PREPARE tools"]
        E["EXECUTE tools (actionId only)"]
    end
    MCP --> R & P & E

    R --> JS["JobService / ProfileService"]
    P --> JS
    P --> AP["ActionPreparer"]
    E --> AX["ActionExecutor"]

    AX --> AS["ActionService<br/>state machine, hash check, CAS, audit"]
    AP --> AS
    JS --> LC["LinkedInClient (interface)"]
    AS -->|"executors"| LC

    LC --> BC["Browser client (Playwright)"]
    LC -.->|"future: official API where one exists"| API["LinkedIn API"]
    BC --> BM["BrowserManager<br/>persistent profile, 1 page, throttled, linkedin.com only"]
    BM --> LI[("linkedin.com")]

    JS & AS & AP --> REPO["Repository interfaces"] --> DB[("SQLite<br/>jobs · job_views · applications · application_answers<br/>profile_changes · messages · actions · audit_events")]

    HUMAN(["You, in a terminal"]) -->|"npm run confirm (TTY + typed code)"| CF["ConfirmationService"] --> AS

Rules the layout enforces:

  • The MCP layer is thin. Tools validate input (zod), call a service, and map errors. No business logic, no SQL, no Playwright.

  • Services depend on interfaces. LinkedInClient hides how LinkedIn is reached; repositories hide where state lives.

  • Only two files import Playwright: src/browser/browser-manager.ts (site-agnostic) and src/linkedin/browser-client.ts (LinkedIn-specific). All DOM knowledge is in src/linkedin/selectors.ts; parsing is pure functions in src/linkedin/parsers/.

  • The confirmation port is not reachable from MCP. See Confirmation workflow.

src/
  index.ts  server.ts  container.ts  config.ts  logger.ts  consent.ts  state-machine.ts
  browser/      browser-manager.ts               persistent context, serialization, throttling, allowlist
  linkedin/     linkedin-client.ts (interface) browser-client.ts session.ts url.ts selectors.ts types.ts
                parsers/ (search, job, profile, dom)
  jobs/ profile/ messaging/ applications/ actions/   services + state machines
  persistence/  database.ts migrations.ts repositories/
  tools/        define.ts schemas.ts views.ts  read/ prepare/ execute/
  cli/          login.ts confirm.ts
  errors/       errors.ts
docs/decisions/ 13 ADRs + index + template        scripts/decisions.mjs
tests/ fixtures/

Why MCP

MCP gives an agent typed, discoverable tools over a standard protocol, so the same server works with Claude Code today and other clients later. It also gives a natural place to put the safety model: the tool surface (what exists, what each accepts) is part of the security boundary. There is no confirm tool, and execute tools accept only an actionId.

Why this technology stack

Layer

Choice

Why (details in the ADR)

Language/runtime

TypeScript on Node 24, run directly via native type stripping

MCP SDK and Playwright are TypeScript-first; no loader dependency (ADR-0001)

MCP

@modelcontextprotocol/server v2, stdio

Current stable line; local transport; no network surface (ADR-0003)

Validation

zod 4 (input and output schemas)

Types vanish at runtime; SDK validates both directions

Browser

Playwright, persistent profile, system Chrome/Edge

Manual login, no credentials handled (ADR-0006)

Parsing

cheerio + pure functions

Fixture-testable without a browser (ADR-0008)

Persistence

node:sqlite behind repository interfaces

No native addon; replaceable by Postgres (ADR-0005)

Tests

vitest + MCP SDK client (in-memory and real stdio)

(ADR-0011)

Four runtime dependencies in total. Logging and state machines are hand-rolled on purpose.

READ / PREPARE / EXECUTE

Kind

May do

May not do

Enforced by

READ

Read LinkedIn; write local tracking (jobs seen, views)

Change LinkedIn; create actions

READ tools are handed only read ports (ActionReader, Pick<JobService,'search'|'get'|'listTracked'>)

PREPARE

Write local state: drafts, PENDING actions, local job status. May read LinkedIn (e.g. current headline)

Perform any external action

PREPARE tools are handed ActionPreparer only: no executor

EXECUTE

Change LinkedIn, for one confirmed action

Accept a payload from the agent; run unconfirmed/expired/altered/repeated actions

ActionExecutor.execute(actionId, type): state machine + hash binding + CAS (ADR-0007)

This is enforced structurally (interfaces handed to each group, state machine, database), not by prompt instructions. The prompt-level guidance in the server instructions is only a courtesy on top.

Security model

Threat model: an LLM (possibly manipulated by hostile text in a job posting) acting only through MCP tools and non-interactive shell, on a machine you control. Out of scope: malware or a person with full access to your account/machine (they could edit the SQLite file or read your browser profile).

Concern

Control

Agent performs an external action unprompted

Execute tools need a CONFIRMED action; only npm run confirm (TTY + typed code) can create one

Agent changes what gets executed after approval

Payload is immutable; its SHA-256 is bound to the confirmation and re-verified at execution (tested by tampering with the DB row)

Replay / double execution

Confirmation is single-use: CONFIRMED → EXECUTING is a database compare-and-swap; no edge back into EXECUTING; race tested

Stale approval

Confirmation expires (default 10 min); pending actions expire (60 min)

Ambiguous write outcome (browser dies mid-submit)

OUTCOME_UNKNOWN blocks any retry until a human reconciles (ADR-0007)

Runaway agent

Hourly cap on executed actions; 25-result cap; single serialized page; spaced navigation

Credential exposure

Passwords, MFA and cookies are never handled by this program: login is manual in a browser window; logs redact sensitive keys; no tool returns them; cookie values are never read (only presence of li_at)

Browser used elsewhere

Host allowlist: only https://*.linkedin.com may be navigated to

Prompt-injected data

Tool inputs are strictly validated; free text is only ever stored/previewed, never executed; LinkedIn URLs are parsed and canonicalised

Error leakage

Unknown errors collapse to INTERNAL_ERROR; paths, page contents and stack traces stay on stderr

Tampering with history

audit_events is append-only (database triggers reject UPDATE/DELETE)

Local secrets

Browser profile, DB and consent live under ~/.linkedin-agent-mcp/ (git-ignored). The profile directory is effectively a credential: protect it

Defence in depth: .claude/settings.json adds Claude Code ask rules for the execute tools.

Installation

Requirements: Node.js ≥ 24, and Google Chrome or Microsoft Edge installed (or npx playwright install chromium and BROWSER_CHANNEL=chromium).

git clone <this repo> linkedin-agent-mcp && cd linkedin-agent-mcp
npm install
npm run build
npm test

Authentication

npm run login
  1. First run only: shows the ToS/automation notice; you must type I UNDERSTAND. (Refuses to run without an interactive terminal.)

  2. Opens a visible Chrome/Edge window on a dedicated profile (~/.linkedin-agent-mcp/browser-profile).

  3. You sign in: password, MFA, and any CAPTCHA (the program never automates or bypasses these).

  4. It detects the session (presence of the li_at cookie and a non-wall page), double-checks by loading the feed, and closes the browser so the profile is saved.

Re-run when the session expires (LOGIN_REQUIRED). Check state any time with the linkedin_auth_status tool.

Configuration

Typed and validated at startup (src/config.ts); see .env.example. No LinkedIn credentials exist in configuration.

Variable

Default

Meaning

LINKEDIN_AGENT_HOME

~/.linkedin-agent-mcp

Root for all local state

LINKEDIN_PROFILE_DIR

<home>/browser-profile

Browser profile

DATABASE_PATH

<home>/agent.db

SQLite file

BROWSER_HEADLESS

true

(npm run login is always visible)

BROWSER_CHANNEL

chrome

chrome | msedge | chromium

LOG_LEVEL

info

debug…silent; always stderr

DEFAULT_MAX_RESULTS

10

Hard cap is 25

MIN_NAVIGATION_INTERVAL_MS

3000

Spacing between page loads

CONFIRMATION_TTL_MINUTES

10

Confirmation validity

EXECUTIONS_PER_HOUR_LIMIT

10

Cap on executed external actions

DEBUG_SNAPSHOTS

false

Save HTML on LINKEDIN_CHANGED for selector repair

npm run confirm reads the same variables. If you override LINKEDIN_AGENT_HOME/DATABASE_PATH for the MCP server, set them in your shell too, or the CLI will look at a different database.

Claude Code integration

Syntax verified against the current Claude Code docs (claude mcp add [options] <name> -- <command> [args...]; options go before the name; -- is mandatory).

Option A: command line (use absolute paths; build first with npm run build):

# Windows example
claude mcp add --scope user linkedin-agent -- node C:\Users\you\code\linkedin-agent-mcp\dist\index.js
# macOS/Linux
claude mcp add --scope user linkedin-agent -- node /home/you/linkedin-agent-mcp/dist/index.js

--scope local (default) = this project only, private; --scope project = writes .mcp.json; --scope user = all your projects. node is invoked directly, so the cmd /c wrapper (needed for npx on native Windows) is not required.

Option B: .mcp.json: a ready-made one is in this repo (works when Claude Code is opened in this folder; the first time it asks you to approve the project server):

{
  "mcpServers": {
    "linkedin-agent": {
      "command": "node",
      "args": ["dist/index.js"]
    }
  }
}

The path is relative on purpose: stdio servers start in the project folder, and ${CLAUDE_PROJECT_DIR} is not expanded in .mcp.json (Node received the literal text and exited, which showed up as CONNECTION_CLOSED; the real error is in %LOCALAPPDATA%\claude-cli-nodejs\Cache\<project>\mcp-logs-linkedin-agent). Run npm run build first, and restart the session (or /mcp → Reconnect) after changing this file.

For use from other projects, put an absolute path in args, or use Option A with --scope user. Verify with claude mcp list, or /mcp inside a session. To debug interactively: npm run inspect (MCP Inspector).

Recommended permissions (.claude/settings.json is included). Keep the execute tools on ask, never allow:

{ "permissions": { "ask": ["mcp__linkedin-agent__linkedin_save_job", "mcp__linkedin-agent__linkedin_unsave_job"] } }

Available tools

Every description is prefixed with its kind. Full schemas are visible via tools/list.

READ

Tool

Purpose

linkedin_auth_status

Is the browser profile signed in? (one page load)

linkedin_search_jobs

keywords, location, postedWithin, workType, employmentType, easyApply, maxResults (≤ 25), onlyNew. Results are deduped by LinkedIn job ID and annotated with isNew / localStatus

linkedin_get_job

Full posting by URL; unknown fields are omitted, never guessed

linkedin_get_saved_jobs

LinkedIn's own Saved-jobs list (My items → Job tracker → Saved), maxResults (≤ 25); paginates LinkedIn's Next control if needed; hasMore flags more beyond that. Reflects results into local tracking (localStatus → saved)

linkedin_get_profile

Your own profile; incompleteSections flags anything not fully read

linkedin_get_company

Company "About" page

linkedin_list_tracked_jobs

Local tracking only (no LinkedIn access)

linkedin_get_action, linkedin_list_actions

State + exact preview of prepared actions

linkedin_get_application

Local state of an in-progress application: status, CV, fields, every question with its answer/source/needsInput

linkedin_preview_application

Exact human-readable payload (job / CV / fields / questions) to review before any confirmation; readyToSubmit false while questions are unanswered

PREPARE (local state only)

Tool

Purpose

linkedin_mark_job

Local status: viewed / considering / rejected

linkedin_prepare_save_job

PENDING save/unsave action

linkedin_prepare_profile_update

Current vs proposed headline/about (reads current value from LinkedIn)

linkedin_draft_message

Stores a draft; nothing is sent

linkedin_prepare_connection_request

Stores a request preview; nothing is sent

linkedin_provide_application_answer

Records the user's own answer to one application question that could not be established automatically; never fabricated

linkedin_cancel_action

Cancels a pending/confirmed action

EXECUTE (requires a user-confirmed action)

Tool

Purpose

linkedin_save_job, linkedin_unsave_job

Take only actionId

Planned, not registered so the agent never meets a dead end: submit_application, update_profile, send_message, send_connection_request, prepare_application. See ADR-0009.

Example

// 1. agent → linkedin_search_jobs
{ "keywords": "NestJS", "location": "Tel Aviv, Israel", "workType": "hybrid", "maxResults": 5, "onlyNew": true }
// ← { "jobs": [{ "id": "3812345678", "title": "...", "company": "...", "isNew": true, "localStatus": "discovered", ... }],
//     "fetched": 5, "newCount": 3, "filteredOut": 2 }

// 2. agent → linkedin_prepare_save_job   { "url": "https://www.linkedin.com/jobs/view/3812345678/" }
// ← { "action": { "actionId": "26751bd3-…", "status": "PENDING", "preview": "SAVE \"…\" at … on LinkedIn" },
//     "executionAvailable": true, "nextStep": "Show the preview… run `npm run confirm -- 26751bd3`…" }

// 3. YOU, in a terminal:  npm run confirm -- 26751bd3      (shows the preview, asks you to type a code)

// 4. agent → linkedin_save_job { "actionId": "26751bd3-…" }   ← COMPLETED (single use)

Confirmation workflow

prepare tool ─► PENDING ──(you: npm run confirm, TTY + code)──► CONFIRMED ──► EXECUTING ──► COMPLETED
     │             │ ▲                                              │            │ ├─► FAILED           (proven: nothing happened)
     │             │ └── payload hash fixed here                    │            │ └─► OUTCOME_UNKNOWN  (ambiguous: never auto-retry)
     │             └─► CANCELLED / EXPIRED                          └─► CANCELLED / EXPIRED (10 min)
     └─ returns actionId + exact preview
  • Why out-of-band? The agent calls the tools, so any in-band confirmed:true can be forged. See ADR-0004.

  • npm run confirm (list) · npm run confirm -- <id> (review + confirm) · -- --cancel <id> · -- --audit <id> · -- --resolve <id> completed|failed (after an OUTCOME_UNKNOWN, once you have checked LinkedIn).

  • "Apply to good jobs", "go ahead with all of them", or any vague instruction is not a confirmation: each action needs its own confirmation of its exact payload.

Application workflow

Target flow: search → inspect → analyze → prepare_application → (needs_user_input | ready_to_submit) → your review → your confirmation → submit_application.

Implemented today: the guarded core: ApplicationService and the application state machine (DISCOVERED → PREPARING → NEEDS_USER_INPUT ⇄ READY_TO_SUBMIT → CONFIRMED → SUBMITTING → SUBMITTED, plus FAILED, SUBMISSION_UNCERTAIN, EXTERNAL, ABANDONED), with these rules enforced and tested: answers are never fabricated (unknown ⇒ needs_input); READY_TO_SUBMIT is impossible with open questions; external applications are detected and reported, never auto-submitted; SUBMITTED is terminal; an uncertain submission can only be resolved, never retried; the preview format (Job / Company / Position / CV / fields / questions / final action) is built by buildPreview. The user-input-collection + preview half of the tool surface is registered (linkedin_get_application, linkedin_provide_application_answer, linkedin_preview_application, 2026-09-22) — but since nothing yet creates an application with real questions (that is prepare_application, next), these tools have no reachable data on the real account until then; they are unit- and protocol-tested against directly-seeded application state.

Not implemented: the Easy Apply form driver, prepare_application and submit_application. A real signed-in session now exists, but no Easy Apply form has been captured yet; the driver is built from a real, read-only capture of one (see Roadmap), starting with prepare_application, never with submit. They are not exposed as tools.

Persistence

SQLite at ~/.linkedin-agent-mcp/agent.db (WAL). Tables: jobs (unique canonical_key = li:<jobId> for dedupe, first_seen_at, last_seen_at, seen_count, status), job_views, applications, application_answers, profile_changes, messages, actions, audit_events (append-only). Job statuses: discovered, viewed, considering, saved, rejected, preparing, ready_to_submit, submitted, failed, changed only through a transition table. Migrations are forward-only (src/persistence/migrations.ts).

Testing

npm test                       # unit + protocol tests, no network
npm run typecheck
LINKEDIN_E2E=true npx vitest run tests/e2e   # OPT-IN, real account, read-only, see file header

PowerShell: $env:LINKEDIN_E2E="true"; npx vitest run tests/e2e

Profile lock. The E2E suite and the MCP server share one persistent browser profile, and Chrome allows one process per profile. If the MCP server has used the browser recently (any browser-backed tool call, including linkedin_auth_status), the E2E run fails immediately with "Could not start the chrome browser" until the server closes its idle browser (BROWSER_IDLE_CLOSE_MS, default 5 min). Run E2E with the MCP server disconnected, or wait.

Covers: input validation through the real MCP SDK client; job parsing (synthetic and real captured fixtures); job-ID extraction and canonicalisation; deduplication (in the database and in the service); application, action and job state machines (including forbidden transitions such as SUBMITTED → PREPARING); the full confirmation workflow (unconfirmed, expired, wrong type, tampered payload, single-use, race, hourly limit, non-TTY, wrong code); failure classification (FAILED vs OUTCOME_UNKNOWN) and audit trail; error mapping and leak checks; logger redaction; the consent gate; repository behaviour incl. append-only triggers; and a real spawned stdio server proving stdout carries only JSON-RPC.

Verification status

Honest accounting of what has and has not been exercised.

Area

Status

Build, typecheck, unit + protocol + spawned-stdio tests

✅ Run, passing

Real Chrome launch with persistent profile; consent gate; session probe

✅ Verified live (signed out)

Job search parsing on real LinkedIn markup, with location filter and maxResults

✅ Verified live, signed-out/guest view (React, Java queries)

Job detail parsing

✅ Verified against a captured real signed-out page (fixture)

Dedupe across separate runs; invalid URL; 404 → JOB_NOT_FOUND; signed-out get_profile/get_company → NOT_AUTHENTICATED; HTTP 999 wall handling

✅ Verified live / with real captured wall

NestJS search

⚠️ Signed out: LinkedIn returned HTTP 999 (throttling); handling of the 999 itself is verified. ✅ Signed in: verified live alongside React and Java (2026-09-20)

Logged-in login, session probe, search, job detail, profile (name, headline, location, About, Experience, Education, Skills incl. the /details/ pages)

✅ Verified live on a real account, 2026-09-20 (tests/e2e, 8/8, read-only). The signed-in layout differs a lot from what was assumed; see ADR-0013

Signed-in fixtures job-detail-signedin.html, profile-signedin.html

⚠️ Modelled on the real markup (structure, hashed classes, componentkey) with invented content; they are regression guards, not raw captures

Save/Unsave click (prepare → npm run confirm → execute → confirmed via get_saved_jobs, both directions)

✅ Verified live once, owner present, on a throwaway job, 2026-09-23; the role-based button lookup worked unchanged

Grouped multi-role experience entries, company page, Easy Apply

❌ Not verified on a real account (the test profile has no grouped roles)

Saved-jobs list (/my-items/saved-jobs/) parsing: entry/company/location split, abbreviated relative dates ("6d ago"), pagination

✅ Probed live (read-only) 2026-09-22; fixture is modelled, not a capture (real content deleted). get_saved_jobs is registered, unit-tested, and verified live by tests/e2e (2026-09-22)

npm run confirm interactive success path

✅ Run by the owner in a real terminal during the Save/Unsave check (2026-09-23). Its logic (ConfirmationService) is also unit-tested, and the non-TTY refusal path was run

Easy Apply, submit, messaging, connection requests, profile writes

⛔ Not implemented

Limitations

  • Selectors will break when LinkedIn changes markup. Failure mode is a typed LINKEDIN_CHANGED, never fake data; DEBUG_SNAPSHOTS=true saves the page for a quick fix.

  • Signed-out pages are throttled by LinkedIn within a few requests (HTTP 999); the intended mode is signed in.

  • The signed-in layout has no <h1>, no stable class names and no <li> in profile lists. It is read through <title>, headings, data-testid and componentkey anchors (ADR-0013); expect these to churn too.

  • linkedin_get_profile is slow by design (main page, lazy-load scrolling, up to three "show all" pages, spaced navigation): tens of seconds. Make sure the MCP client's tool timeout allows it.

  • The E2E suite and the MCP server cannot use the browser profile at the same time (see Testing).

  • In signed-out views an "Apply" button is ambiguous, so easyApply/applyType are reported as unknown.

  • One browser page at a time; a search returns at most 25 results by design.

  • node:sqlite is still flagged experimental by Node.

  • The confirmation model does not protect against something with full shell/database access to your machine.

LinkedIn API limitations

Officially available to any developer: Sign in with LinkedIn (OpenID Connect) and Share on LinkedIn. Job-related APIs (Talent Solutions: Job Posting, Apply Connect, Recruiter System Connect, …) are partner-gated and built for employers/ATS vendors. There is no official API for a job seeker to search jobs, read saved jobs, apply, message, or read their own full profile. LinkedInClient is the seam to swap in an official API if that changes.

Browser automation limitations

Violates LinkedIn ToS §8.2 on its face (see top); subject to bot detection (HTTP 999, CAPTCHA, verification checkpoints: all surfaced as typed errors and never bypassed); DOM churn; sessions expire; needs a real browser installed.

Roadmap

Done: signed-in read-only flows verified live and parsers fixed (2026-09-20, ADR-0013); Chrome-exit-at-launch now classified as a profile lock, not "could not start Chrome" (2026-09-22); get_saved_jobs implemented, unit-tested against a modelled fixture, registered, and verified live by tests/e2e (2026-09-22); user-input-collection + application-preview tools (linkedin_get_application, linkedin_provide_application_answer, linkedin_preview_application) implemented, registered, unit- and protocol-tested (2026-09-22) — built ahead of prepare_application, so not yet reachable on the real account; Save/Unsave verified live end to end with the owner present (2026-09-23).

Next, in order (each gated on live verification, read-only first; the same list with working notes is in CLAUDE.md):

  1. prepare_application: open a real Easy Apply form read-only, detect fields/questions, fill only what the profile/CV/config establishes, stop at needs_user_input, detect external apply. This is where the agentic logic starts, and the first thing that will actually feed data into linkedin_get_application/linkedin_provide_application_answer/linkedin_preview_application. Do not start with submit_application.

  2. submit_application with idempotency checks (inspect LinkedIn's "Applied" state before any retry).

  3. Only then update_profile, send_message, send_connection_request.

Later: richer job tracking (notes, follow-ups); optional MCP elicitation as a second confirmation channel; PostgreSQL repositories if ever multi-user.

Responsible use

This is for your own account and a small number of deliberate actions. Do not use it to mass-apply, mass-message, mass-connect or harvest data. Do not point it at other people's accounts. Do not add stealth, CAPTCHA solving or rate-limit evasion: when LinkedIn says no (999, checkpoint, CAPTCHA), stop. You are responsible for compliance with LinkedIn's terms and applicable law, and for the accuracy of anything submitted in your name. Never let an agent fabricate answers on applications; the design makes it ask you.

Decision log

Every significant choice, with options considered and an interview soundbite: docs/decisions/ · docs/INTERVIEW.md · npm run decisions.

License

MIT. The licence covers this code only; using it against LinkedIn is still subject to their User Agreement (see Responsible use).

Available Tools

20 tools
linkedin_auth_statusLinkedIn sign-in statusA
Read-onlyIdempotent

[READ - no LinkedIn state is changed] Checks whether the persistent browser profile is signed in to LinkedIn. Use it to diagnose NOT_AUTHENTICATED / LOGIN_REQUIRED errors. Never returns cookies or tokens. If not signed in, the USER must run npm run login; the agent cannot log in.

ParametersJSON Schema
NameRequiredDescriptionDefault

No parameters

Output Schema

ParametersJSON Schema
NameRequiredDescription
reasonNo
authenticatedYes

TDQS

A4.7/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already declare readOnlyHint=true, destructiveHint=false and idempotentHint=true, so safety is covered. The description adds genuinely useful, non-redundant context: it returns no cookies or tokens, and the agent has no capability to log in itself. It omits how the check is performed or whether results are cached, which is why this sits at 4 rather than 5.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Four short sentences, front-loaded with the read-only constraint, then purpose, then the security boundary, then remediation. Every sentence carries distinct information and none is padded.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

An output schema exists, so return values need not be described; the description nonetheless clarifies the negative case ('not signed in') and the agent-capability boundary. For a zero-parameter diagnostic tool with annotations covering safety, this is complete.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The tool takes zero parameters, so per the rubric the baseline is 4; there is nothing for the description to clarify beyond the schema, and it correctly does not invent parameter guidance.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb+resource: checks whether the persistent browser profile is signed in to LinkedIn. No sibling tool addresses authentication state, so it is trivially distinguishable, and the leading '[READ - no LinkedIn state is changed]' frames the operation type immediately.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Explicitly says when to use it ('diagnose NOT_AUTHENTICATED / LOGIN_REQUIRED errors') and what to do when it reports a negative result ('the USER must run `npm run login`'). The when-to-use condition and the follow-up path are both stated, leaving nothing to inference.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

linkedin_cancel_actionCancel a prepared actionA

[PREPARE - changes local state only; performs NO external action] Cancels a PENDING or CONFIRMED action so it can never be executed (local state only). Use when the user changes their mind or the draft is superseded.

ParametersJSON Schema
NameRequiredDescriptionDefault
actionIdYes

Output Schema

ParametersJSON Schema
NameRequiredDescription
typeYes
errorNo
statusYes
previewYesExactly what will happen if the user confirms and the action is executed.
actionIdYes
createdAtYes
expiresAtYes
completedAtNo
confirmedAtNo

TDQS

A4.1/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already declare openWorldHint=false and destructiveHint=false, and the description reinforces this by stressing 'local state only; performs NO external action' twice. It also adds state-machine detail (PENDING/CONFIRMED) that the annotations do not convey. No contradiction, and it contributes beyond the structured fields.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Front-loads the critical PREPARE/local-only caveat in brackets, then the operation and its usage in two short sentences. Every clause earns its place with no redundancy.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

With an output schema present, return values need no explanation, and the description covers purpose, effect, and usage. It is nearly complete for a one-parameter tool; the only gap is the unexplained actionId semantics.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters2/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

There is a single parameter (actionId) with 0% schema description coverage, so the description must compensate and largely does not. It only implies actionId refers to an action via 'PENDING or CONFIRMED action'; it never explains ID format, source, or the 8-64 char constraint.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb (cancels) and a precisely scoped resource (a PENDING or CONFIRMED action), including the consequence ('can never be executed'). This clearly separates it from siblings like linkedin_get_action and linkedin_list_actions, which read rather than cancel.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Explicitly gives the triggering context: 'Use when the user changes their mind or the draft is superseded.' That is clear when-to-use guidance, though it names no explicit alternative tool for related operations.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

linkedin_draft_messageDraft a messageA

[PREPARE - changes local state only; performs NO external action] Stores a DRAFT LinkedIn message (max 2000 chars) to a recipient for the user to review. Nothing is sent. Draft only when the user asked for a message to a specific person; never because a recruiter was merely found. This version cannot send messages: show the draft so the user can send it manually.

ParametersJSON Schema
NameRequiredDescriptionDefault
messageYes
recipientUrlYesRecipient's profile URL, e.g. https://www.linkedin.com/in/jane-doe/. Must be an https://www.linkedin.com URL.
recipientNameYes

Output Schema

ParametersJSON Schema
NameRequiredDescription
actionYes
messageYes
nextStepYes
recipientYes
recipientUrlYes
executionAvailableYesFalse when this version cannot execute the action; the user must do it manually.

TDQS

A4.7/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations only declare generic hints (readOnlyHint=false, destructiveHint=false, openWorldHint=false); the description adds the substantive behavior: changes local state only, nothing is transmitted, and the tool cannot send at all. It also states the 2000-char cap and that the draft must be surfaced to the user. No contradiction with the annotations, since writing a local draft is consistent with readOnlyHint=false.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Front-loaded with the bracketed safety tag, then purpose, then usage rules, then workflow instruction. Every clause earns its place; no filler.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

With an output schema present, return values need no explanation; the description covers the safety profile, recipient/length constraints, and the user-review workflow an agent needs to invoke this correctly. No material gap remains for a 3-param local-state tool.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 33% (only recipientUrl documented in the schema). The description restates the 2000-char message limit but adds no meaning for recipientName or recipientUrl beyond the schema, so it only partially compensates for the coverage gap. Baseline 3 for a low-coverage 3-param tool.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb+resource ('Stores a DRAFT LinkedIn message') with an explicit [PREPARE] tag and the crucial scope limit 'performs NO external action; Nothing is sent'. It clearly distinguishes itself from send/action siblings and from the found-recruiter case.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Gives an explicit when-to-use condition ('only when the user asked for a message to a specific person') and an explicit when-not ('never because a recruiter was merely found'), plus the follow-up workflow ('show the draft so the user can send it manually'). Nothing is left to inference.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

linkedin_get_actionGet a prepared actionA
Read-onlyIdempotent

[READ - no LinkedIn state is changed] Returns the state and exact preview of a prepared action (local state only). Use it to check whether the user has confirmed an action, or after an OUTCOME_UNKNOWN error.

ParametersJSON Schema
NameRequiredDescriptionDefault
actionIdYes

Output Schema

ParametersJSON Schema
NameRequiredDescription
typeYes
errorNo
statusYes
previewYesExactly what will happen if the user confirms and the action is executed.
actionIdYes
createdAtYes
expiresAtYes
completedAtNo
confirmedAtNo

TDQS

A4/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already declare readOnlyHint, idempotentHint and destructiveHint=false, so the safety profile is covered. The description still adds value beyond them: it clarifies that only local state is involved and no LinkedIn state is changed, and it names the OUTCOME_UNKNOWN scenario as a reason to call it.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Three short clauses, zero filler, with the read-only constraint and local-state scope front-loaded before the usage triggers. Every sentence earns its place.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

An output schema exists, so return values need not be described, and the description still usefully frames what comes back (state and exact preview). For a one-parameter read tool with full annotations, this is nearly complete; only the actionId sourcing is left implicit.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Single parameter with 0% schema description coverage, so the description carries the burden. It implies actionId refers to a previously 'prepared action', but never explains the id format, length constraints, or where to obtain it (e.g., from linkedin_list_actions output). Minimal viable semantics rather than compensating for the gap.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb and resource ('Returns the state and exact preview of a prepared action') and pins the scope as local state only, which separates it from state-changing siblings like linkedin_save_job or linkedin_prepare_save_job. It does not explicitly name the nearby sibling it could be confused with (linkedin_list_actions), so it falls just short of a 5.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Gives two concrete trigger conditions: checking whether a user has confirmed an action, and recovering after an OUTCOME_UNKNOWN error. This is clear when-to-use guidance, but there are no explicit when-not conditions or named alternatives.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

linkedin_get_applicationGet an applicationA
Read-onlyIdempotent

[READ - no LinkedIn state is changed] Returns the local state of an in-progress application: status, CV, fields and every question with its answer/source (profile, config, user, or proposed) and whether it still needsInput. Local state only, never touches LinkedIn.

ParametersJSON Schema
NameRequiredDescriptionDefault
applicationIdYes

Output Schema

ParametersJSON Schema
NameRequiredDescription
jobIdYes
fieldsYes
statusYes
answersYes
cvLabelYes
applicationIdYes
unansweredCountYes

TDQS

A3.8/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already declare readOnlyHint, idempotentHint, non-destructive and closed-world. The description adds real value: it clarifies that only local state is read and LinkedIn is never contacted (a stronger claim than readOnlyHint alone) and describes the shape of the returned state including answer provenance and needsInput flags.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Front-loaded with the [READ] safety marker, then two dense sentences that each carry useful detail with no filler or repetition.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a single-parameter read tool with an output schema and full annotation coverage, the description is essentially complete; the only minor gap is any hint about the applicationId format/lookup, which the schema also leaves blank.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 0% for the single applicationId parameter, so the schema does not document it. The name is largely self-explanatory and the description says nothing further about it, which is adequate but not compensating for the coverage gap.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb (Returns/Get) and resource (local state of an in-progress application), then enumerates the returned content: status, CV, fields, questions with answer/source and needsInput. The phrase 'local state only, never touches LinkedIn' implicitly distinguishes it from siblings like linkedin_preview_application, though no sibling is named explicitly.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines3/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Usage is implied by 'in-progress application', which tells the agent the applicable context, but there is no explicit when-to-use/when-not guidance and no routing to alternatives such as linkedin_preview_application or linkedin_provide_application_answer.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

linkedin_get_companyGet a LinkedIn companyA
Read-onlyIdempotent

[READ - no LinkedIn state is changed] Reads a company's public LinkedIn 'About' page (description, industry, size, headquarters, website).

ParametersJSON Schema
NameRequiredDescriptionDefault
urlYesCompany page URL, e.g. https://www.linkedin.com/company/acme/. Must be an https://www.linkedin.com URL.

Output Schema

ParametersJSON Schema
NameRequiredDescription
nameYes
websiteNo
industryNo
locationNo
descriptionNo
linkedinUrlYes
employeeCountNo

TDQS

A3.8/5.0
Behavior3/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already declare readOnlyHint, idempotentHint, openWorldHint and destructiveHint=false, so the '[READ - no LinkedIn state is changed]' prefix largely restates structured data. The one genuinely additive fact is that only the public 'About' section is retrieved, which hints at limited result scope, but nothing about failure modes or rate limits is given.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

One sentence, front-loaded with the read-only guarantee, then the resource and its payload. No filler or redundancy.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

With an output schema present, return-value documentation is not required, and the description still lists the expected fields. For a one-parameter read tool this is essentially complete, with only usage routing left implicit.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100% and the single url parameter is fully documented in the schema (format, https://www.linkedin.com requirement, example). The description adds no syntax or format detail beyond it, so the baseline 3 applies.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

Specific verb (reads) plus resource (company's public LinkedIn 'About' page) and an explicit enumeration of the returned fields. It is clearly distinguishable from siblings like linkedin_get_profile or linkedin_get_job.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines3/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description establishes context by labeling the tool READ-only, but it gives no when-to-use condition, no prerequisite, and no distinction from siblings that also fetch LinkedIn entities. Usage is implied rather than stated.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

linkedin_get_jobGet a LinkedIn jobA
Read-onlyIdempotent

[READ - no LinkedIn state is changed] Opens one LinkedIn job posting and returns its full description and metadata, plus local tracking info. Fields LinkedIn does not show are omitted (never guessed). Marks the job as "viewed" locally. Use it before analysing a job against the user's CV.

ParametersJSON Schema
NameRequiredDescriptionDefault
urlYesJob posting URL, e.g. https://www.linkedin.com/jobs/view/3812345678/. Must be an https://www.linkedin.com URL.

Output Schema

ParametersJSON Schema
NameRequiredDescription
idNo
urlYes
titleYes
postedNo
salaryNo
skillsNo
companyYes
locationNo
trackingYes
workTypeNo
applyTypeNo
easyApplyNo
seniorityNo
descriptionYes
employmentTypeNo

TDQS

A4.3/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already declare readOnly/ idempotent/ non-destructive, so the safety profile is covered. The description adds non-obvious behavior beyond that: a local side effect ('Marks the job as "viewed" locally') and a data-fidelity policy ('Fields LinkedIn does not show are omitted (never guessed)'). Auth requirements and failure modes are not covered, keeping it at 4 rather than 5.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Four short sentences, front-loaded with the '[READ - no LinkedIn state is changed]' tag so the safety signal lands first. Every sentence adds distinct information (state safety, return content, field policy, local side effect, usage timing) with no filler.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a single-parameter read tool with full annotations and an output schema, the description covers what an agent needs: what it fetches, that remote state is untouched, that a local viewed-flag is set, how missing fields are handled, and when to call it. Return-value detail is rightly left to the output schema.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

There is a single required parameter with 100% schema description coverage, including format and URL constraints, so the schema carries the meaning. The description adds nothing about the url parameter itself, which is the expected baseline-3 outcome when coverage is complete.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description uses a specific verb+resource ('Opens one LinkedIn job posting and returns its full description and metadata') and scopes it to a single posting, which cleanly separates it from linkedin_search_jobs and linkedin_get_saved_jobs. What is returned is stated concretely, so an agent can tell what this tool does without opening the schema.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

It gives a clear condition of use: 'Use it before analysing a job against the user's CV.' That is a real usage cue, but it does not name competing alternatives (e.g. search_jobs vs get_saved_jobs) or state when not to use it, so it falls short of the explicit when/when-not routing a 5 requires.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

linkedin_get_profileGet my LinkedIn profileA
Read-onlyIdempotent

[READ - no LinkedIn state is changed] Reads the signed-in user's OWN profile (headline, about, experience, education, skills). incompleteSections lists anything that could not be read completely; treat those parts as unknown, not empty.

ParametersJSON Schema
NameRequiredDescriptionDefault

No parameters

Output Schema

ParametersJSON Schema
NameRequiredDescription
nameNo
aboutNo
skillsNo
headlineNo
locationNo
educationNo
experienceNo
incompleteSectionsNo

TDQS

A4.5/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already declare readOnlyHint, idempotentHint, and destructiveHint=false, so the '[READ - no LinkedIn state is changed]' line is partly redundant. However, the incompleteSections caveat is genuine added value: it warns the agent that partial results must be treated as unknown rather than empty, which is not derivable from the annotations.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Two tight sentences with no filler; the safety marker and scope come first, followed by the returned fields and the caveat. Every clause earns its place.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

With an output schema present, the description need not explain return structure, and it correctly avoids that. Scope, safety, and the partial-read caveat cover everything an agent needs to call and interpret this no-parameter read tool.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The tool takes zero parameters, and the extended schemas correctly apply the baseline of 4 when there is nothing to document. The description's field list describes output rather than input, so it neither helps nor hurts here.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb (Reads) and a precisely scoped resource (the signed-in user's OWN profile), then enumerates the fields returned (headline, about, experience, education, skills). This clearly separates it from siblings like linkedin_get_company (other entities) and linkedin_prepare_profile_update (writes).

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The OWN-profile scoping and the READ marker give clear context for when this tool applies, and the sibling set makes read-vs-write intent obvious. It stops short of naming an explicit alternative or a when-not condition, so it does not reach a 5.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

linkedin_get_saved_jobsGet my LinkedIn saved jobsA
Read-onlyIdempotent

[READ - no LinkedIn state is changed] Reads LinkedIn's own Saved-jobs list (My items -> Job tracker -> Saved), i.e. jobs actually saved on the real account right now, not just local tracking. Each result is recorded in local tracking (localStatus becomes "saved" unless it was already further along, e.g. submitted). hasMore=true means there are more saved jobs than maxResults returned; call again with a larger maxResults to see them.

ParametersJSON Schema
NameRequiredDescriptionDefault
maxResultsNo1-25. Default is small on purpose; this is not a scraper.

Output Schema

ParametersJSON Schema
NameRequiredDescription
jobsYes
fetchedYes
hasMoreYesTrue when LinkedIn has more saved jobs beyond this page (increase maxResults to see more).

TDQS

A4.6/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Beyond the readOnlyHint annotation, the description discloses a non-obvious side effect: every returned saved job is written into local tracking (localStatus becomes 'saved' unless already further along). It also explains the hasMore pagination contract, which is behavioral information the annotations do not cover.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Front-loaded with a bracketed [READ] marker and a single dense paragraph; every sentence carries information (scope, side effect, pagination). The sentences are long and pack several ideas together, which slightly hurts skimability but there is no filler.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Scope, the local-tracking side effect, and the pagination loop are all covered, and the presence of an output schema means return values need no further explanation. An agent has everything required to call this correctly.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100% and the schema already documents maxResults' 1-25 range and default, so the baseline is 3. The description adds relational meaning by tying maxResults to hasMore and the retry pattern, which the schema alone does not convey.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb and resource ('Reads LinkedIn's own Saved-jobs list') and explicitly scopes it ('jobs actually saved on the real account right now, not just local tracking'), which separates it from the local-tracking siblings. An agent can identify the tool's job without opening the schema.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

It gives clear context for use: read the real saved list, and if hasMore=true call again with a larger maxResults. It implicitly contrasts with local tracking but never names the alternative sibling (e.g. linkedin_list_tracked_jobs) as an explicit routing choice, so it falls short of the 5 bar.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

linkedin_list_actionsList prepared actionsA
Read-onlyIdempotent

[READ - no LinkedIn state is changed] Lists recent prepared actions and their states (local state only), optionally filtered by status.

ParametersJSON Schema
NameRequiredDescriptionDefault
limitNo
statusNo

Output Schema

ParametersJSON Schema
NameRequiredDescription
actionsYes

TDQS

A3.8/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already declare readOnly/idempotent/non-destructive, so the safety profile is covered and the '[READ - no LinkedIn state is changed]' prefix largely repeats it. The genuinely additive detail is '(local state only)', which tells the agent these are staged local records rather than live LinkedIn data — a meaningful behavioral clarification.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

A single front-loaded sentence with the read-only constraint first and the filter optionality last; every clause adds information and nothing is wasted.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

With an output schema present, return values need not be described, and the description covers scope, safety, and filtering adequately. It could note ordering or the default page size implied by 'recent', but nothing critical to correct invocation is missing.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 0% for 2 parameters, and the description only restates the status filter without explaining its enum values or the meaning/default of 'limit' (max 50). Baseline 3 is warranted since the enum carries the filter values but limit stays fully undocumented in prose.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb and resource ('Lists recent prepared actions and their states') and scopes it as local-only, which distinguishes it from the single-record linkedin_get_action. It never names a sibling explicitly, so differentiation still relies on the reader inferring from the plural 'actions'.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines3/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

'Optionally filtered by status' implies when to narrow results, but there is no explicit when-to-use framing or routing against alternatives such as linkedin_get_action for a single action. Usage must be inferred.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

linkedin_list_tracked_jobsList locally tracked jobsA
Read-onlyIdempotent

[READ - no LinkedIn state is changed] Lists jobs from LOCAL tracking only (no LinkedIn access), optionally by local status (discovered, viewed, considering, saved, rejected, ...). Use it to avoid re-presenting jobs the user already handled.

ParametersJSON Schema
NameRequiredDescriptionDefault
limitNo
statusNo

Output Schema

ParametersJSON Schema
NameRequiredDescription
jobsYes

TDQS

A4/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already cover readOnly, idempotent, non-destructive, and closed-world hints. The description adds that no LinkedIn state is changed and that it operates on LOCAL tracking only, reinforcing that it doesn't touch the remote platform. It does not mention pagination or ordering, but the output schema likely covers return shape, so the gap is minor.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Two sentences, front-loaded with the READ qualifier and local-only scope, then the usage guidance. No wasted words; every phrase adds value.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a simple list tool with an output schema and annotations covering safety, the description provides enough context: local-only scope, status filtering, and usage intent. It could mention pagination or default behavior for limit, but those are minor given the output schema exists.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 0%, so the schema itself provides no help. The description partially compensates by explaining the 'status' parameter meaning (local status values like discovered, viewed, etc.) and implying 'limit' via 'optionally by local status'. But it doesn't explain 'limit' or clarify that status is a filter. Baseline 3 is appropriate given partial compensation.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb and resource ('Lists jobs from LOCAL tracking'). It clarifies the local-only scope, which distinguishes it from sibling tools like linkedin_search_jobs or linkedin_get_saved_jobs. However, it doesn't explicitly name which sibling to use for LinkedIn-side job retrieval.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Provides clear context: 'Use it to avoid re-presenting jobs the user already handled.' This gives a concrete when-to-use scenario. But it doesn't contrast with alternatives like linkedin_get_saved_jobs or explain when NOT to use it (e.g., to fetch remote jobs).

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

linkedin_mark_jobMark a job locallyA

[PREPARE - changes local state only; performs NO external action] Sets the LOCAL tracking status of a job the agent already knows (viewed, considering or rejected). Does not touch LinkedIn. Use "rejected" only when the USER said they are not interested, so it is not shown again.

ParametersJSON Schema
NameRequiredDescriptionDefault
urlYesJob URL. Must be an https://www.linkedin.com URL.
statusYes

Output Schema

ParametersJSON Schema
NameRequiredDescription
idNo
titleYes
statusYes
companyYes

TDQS

A4.6/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations declare readOnlyHint=false and destructiveHint=false, but do not explain WHAT is mutated. The description supplies the critical missing context: this is a local-state-only write with no external side effect, plus a durable consequence ('so it is not shown again'). That is meaningful disclosure beyond the annotations, not a restatement.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Front-loads the most decision-relevant fact (local only, no external action) in a bracketed prefix, then states the resource and the one risky enum rule. No filler sentences; every clause carries routing or behavioral information.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Return values need not be explained since an output schema exists, and the local-vs-remote distinction is fully covered. Minor gaps remain: no mention of overwrite/idempotency behavior when re-marking the same job, and no note on what happens to the url parameter's sibling linkage.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 50% (url documented, status enum undocumented). The description compensates by explaining the enum semantics ('viewed, considering or rejected') and adding a decision rule for the non-obvious value 'rejected', which the schema's bare enum cannot convey.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb and resource ('Sets the LOCAL tracking status of a job') and immediately scopes it as a local-only write that performs NO external action. This differentiates it from the mutating siblings linkedin_save_job and linkedin_unsave_job, which an agent would otherwise confuse it with.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Gives explicit when-to-use guidance, including a strong conditional rule: use "rejected" only when the USER said they are not interested. It also clarifies the negative scope (does not touch LinkedIn), though it does not name a specific alternative tool to use for external actions.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

linkedin_prepare_connection_requestPrepare a connection requestA

[PREPARE - changes local state only; performs NO external action] Prepares (does NOT send) a LinkedIn connection request with an optional note (max 300 chars). Only for a specific person the user named. Never bulk-prepare. This version cannot send connection requests: show the preview so the user can do it manually.

ParametersJSON Schema
NameRequiredDescriptionDefault
noteNo
recipientUrlYesRecipient's profile URL. Must be an https://www.linkedin.com URL.
recipientNameYes

Output Schema

ParametersJSON Schema
NameRequiredDescription
actionYes
nextStepYes
executionAvailableYesFalse when this version cannot execute the action; the user must do it manually.

TDQS

A4.4/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

The annotations only tell the agent it is a non-destructive mutation; the description adds the crucial facts that it changes local state only, performs no external action, and cannot actually send. That 'no external action' guarantee is a behavioral trait the annotations do not convey and materially affects how the agent should present the result.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Front-loaded bracketed tag followed by dense but purposeful sentences; nothing is filler. Slightly heavy punctuation, but appropriately sized for a state-changing preparation step.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

An output schema exists so return values need no explanation, and the safety profile is covered by annotations. Combined with the explicit no-send/no-bulk constraints, the description gives the agent everything needed to call and frame this tool correctly.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is low (33%), and the description only restates the note's 300-char limit (already in the schema) and implies a named recipient. It does not explain recipientUrl, its LinkedIn-URL requirement, or how the agent should source those values, leaving the schema to carry most parameter meaning.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb (prepares, not sends) and resource (a LinkedIn connection request with optional note), and immediately distinguishes itself from a send action and from bulk preparation. An agent would not confuse it with any sibling.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Explicit conditions are given: 'Only for a specific person the user named' and 'Never bulk-prepare,' plus the instruction to show the preview so the user acts manually. It doesn't name a specific sibling alternative, but the constraint set is clear.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

linkedin_prepare_profile_updatePrepare a profile changeA

[PREPARE - changes local state only; performs NO external action] Prepares (does NOT apply) a change to the user's own LinkedIn headline (max 220 chars) or about section (max 2600 chars). Reads the current value from LinkedIn and returns current vs proposed as a PENDING action. Never propose changes the user did not ask for. This version cannot apply profile changes: show the preview so the user can apply it manually.

ParametersJSON Schema
NameRequiredDescriptionDefault
fieldYes
proposedValueYes

Output Schema

ParametersJSON Schema
NameRequiredDescription
fieldYes
actionYes
nextStepYes
currentValueYes
proposedValueYes
executionAvailableYesFalse when this version cannot execute the action; the user must do it manually.

TDQS

A4.8/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Discloses beyond the annotations that it mutates only local state, performs no external action, reads the current LinkedIn value, and returns a PENDING action rather than applying it. This resolves the tension around readOnlyHint=false (it does create a local pending action) rather than contradicting it.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Front-loaded bracketed scope marker plus tight sentences, but the no-apply constraint is restated twice ('does NOT apply' and 'cannot apply profile changes'), which is mild redundancy.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

With an output schema present, return-shape detail is unnecessary, yet the description still usefully says it returns current vs proposed as a pending action. For a two-field local-state tool, nothing an agent needs to call it correctly is missing.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 0%, so the description must compensate, and it does: it names the two valid fields and gives field-specific limits (220 for headline, 2600 for about) that the schema only captures as a generic maxLength of 2600. It does not elaborate on proposedValue formatting, so it stops short of full compensation.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb (prepares) and resource (user's own LinkedIn headline or about section), and explicitly demarcates the PREPARE-only scope versus applying. An agent can distinguish it from apply-capable siblings without opening any schema.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Explicitly states when to use it (to produce a preview) and when it must not be treated as final, routing the user to manual application. It also sets the boundary 'never propose changes the user did not ask for', leaving no inference needed.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

linkedin_prepare_save_jobPrepare saving/unsaving a jobA

[PREPARE - changes local state only; performs NO external action] Prepares (does NOT perform) saving or un-saving a job on LinkedIn. Returns a PENDING action with a preview. Recommending a job does not require saving it; only prepare this when the user asked to save it. To execute: the user confirms in their terminal, then call linkedin_save_job / linkedin_unsave_job.

ParametersJSON Schema
NameRequiredDescriptionDefault
urlYesJob URL. Must be an https://www.linkedin.com URL.
modeNosave

Output Schema

ParametersJSON Schema
NameRequiredDescription
actionYes
nextStepYes
executionAvailableYesFalse when this version cannot execute the action; the user must do it manually.

TDQS

A4.7/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Beyond the annotations, the description discloses the key behavioral distinction: it 'changes local state only; performs NO external action' and returns 'a PENDING action with a preview.' It also explains the confirmation workflow required before execution, which is crucial for a non-idempotent, non-destructive prepare tool.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is front-loaded with a bracketed PREPARE warning, then states purpose, behavior, usage constraint, and execution path. Every sentence adds necessary information; none is redundant.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the output schema exists, the description needn't explain return values fully, yet it still notes the PENDING action and preview. With annotations and schema present, the description covers the essential local-state-only behavior, confirmation workflow, and sibling execution tools, leaving no critical gaps.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 50%: the url parameter is documented in the schema, but the mode enum lacks a description. The description's wording 'saving or un-saving' loosely maps to the save/unsave modes, but it adds no syntax, default behavior, or constraint details beyond the schema.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description states a specific verb and resource: 'Prepares (does NOT perform) saving or un-saving a job on LinkedIn.' It immediately distinguishes itself from the actual execution tools by naming linkedin_save_job and linkedin_unsave_job, so an agent can tell it apart without opening schemas.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

It gives explicit when-to-use guidance ('only prepare this when the user asked to save it'), a when-not condition ('Recommending a job does not require saving it'), and the exact execution path ('the user confirms in their terminal, then call linkedin_save_job / linkedin_unsave_job'). Alternatives and sequencing are both covered.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

linkedin_preview_applicationPreview an application before submissionA
Read-onlyIdempotent

[READ - no LinkedIn state is changed] Renders the exact human-readable payload for an application (job, CV, fields, every question and its answer) so the user can review it before any confirmation. readyToSubmit is false while any question is unanswered; use linkedin_provide_application_answer for those first. Local state only, never touches LinkedIn.

ParametersJSON Schema
NameRequiredDescriptionDefault
applicationIdYes

Output Schema

ParametersJSON Schema
NameRequiredDescription
statusYes
previewYesExact human-readable payload the user should review before any confirmation.
unansweredYesQuestions still needing the user's answer, if any.
readyToSubmitYesTrue once every question has an established or user-supplied answer.

TDQS

A4.3/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already declare readOnlyHint, idempotentHint, and destructiveHint=false, so the safety profile is covered. The description still adds real behavioral context beyond them: it discloses that only local state is touched, that no LinkedIn state changes, and the readyToSubmit gating semantics. It does not describe output shape, but an output schema exists for that.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Three sentences, front-loaded with a bracketed [READ] tag and the core action. There is mild redundancy — '[no LinkedIn state is changed]' and 'never touches LinkedIn' convey the same fact — but nothing is wasted verbatim.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

An output schema exists, so return-value explanation is not required. The description covers purpose, timing, prerequisites, and state guarantees; the only gap is any hint about how to obtain a valid applicationId, which is minor for a low-complexity read tool.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 0% and the single applicationId parameter has no schema description, so the description must compensate. It only implies the parameter identifies the application to preview; it gives no guidance on the ID's origin, format, or the min/max length constraints. This is the minimum-viable baseline for a one-parameter tool.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb and resource ('Renders the exact human-readable payload for an application') and enumerates what the preview contains (job, CV, fields, every question and answer). It clearly distinguishes itself from the sibling linkedin_provide_application_answer, which it names directly.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Explicitly states the moment to use it ('so the user can review it before any confirmation') and routes the agent to the alternative when a prerequisite is unmet ('readyToSubmit is false while any question is unanswered; use linkedin_provide_application_answer for those first'). When/when-not and the alternative are all present.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

linkedin_provide_application_answerAnswer an application questionA

[PREPARE - changes local state only; performs NO external action] Records the USER's own answer to one application question that could not be established from their profile/CV/config. Local state only, nothing is sent to LinkedIn. Never invent the answer yourself: only call this with an answer the user actually gave. Once every question is answered the application becomes READY_TO_SUBMIT; check with linkedin_preview_application.

ParametersJSON Schema
NameRequiredDescriptionDefault
answerYes
answerIdYes
applicationIdYes

Output Schema

ParametersJSON Schema
NameRequiredDescription
jobIdYes
fieldsYes
statusYes
answersYes
cvLabelYes
applicationIdYes
unansweredCountYes

TDQS

A4.6/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations flag readOnlyHint=false and openWorldHint=false; the description confirms and enriches this by declaring the operation is local-state-only and that nothing is sent to LinkedIn, plus it discloses the downstream state machine effect (application becomes READY_TO_SUBMIT once all questions are answered). That is behavioral context beyond what the annotations convey.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Front-loaded with the scope tag and operation, then constraints, then the state-transition and cross-reference. Every clause carries information; no filler.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Output schema exists so return values need no explanation, and the safety/scope profile is fully covered. The only gap is that the agent isn't told how to obtain valid applicationId/answerId values, which is a real if minor omission for a 3-required-param tool.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 0% and no parameter descriptions exist, so the description must compensate; it clarifies the intent of 'answer' (must be a real user-provided answer, not generated) but gives no guidance on where applicationId or answerId come from, nor the 2000/64-char limits. Partial compensation only.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific action ('records the USER's own answer to one application question') plus a bracketed scope tag [PREPARE - changes local state only; performs NO external action] that separates it from the sibling prepare/save tools. It also names the state transition target (READY_TO_SUBMIT) so the agent knows what this call accomplishes.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Explicitly scopes when to use it ('one application question that could not be established from their profile/CV/config'), states a hard prohibition ('Never invent the answer yourself: only call this with an answer the user actually gave'), and routes to the verification sibling ('check with linkedin_preview_application').

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

linkedin_save_jobSave a job on LinkedInA

[EXECUTE - external action; REQUIRES the user to have confirmed this exact action] Saves a job on LinkedIn (changes LinkedIn state; reversible with linkedin_unsave_job). Requires an actionId from linkedin_prepare_save_job that the USER has confirmed in their own terminal. Fails with CONFIRMATION_REQUIRED otherwise; do not retry in a loop, ask the user. Never save jobs just because they look good.

ParametersJSON Schema
NameRequiredDescriptionDefault
actionIdYesThe actionId returned by the matching prepare tool, after the user confirmed it.

Output Schema

ParametersJSON Schema
NameRequiredDescription
dataNo
actionYes
summaryYes

TDQS

A4.6/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations only tell the agent this is a non-read-only, non-idempotent, non-destructive write. The description adds the crucial operational context they lack: user confirmation is mandatory, the specific CONFIRMATION_REQUIRED error, and that retrying is harmful because the call is not idempotent. That is genuine value beyond the structured fields.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Front-loaded with the EXECUTE warning and a dense but single-paragraph body where each clause earns its place (precondition, failure, no-retry, reversal). The closing 'Never save jobs just because they look good' is slightly editorial but still functional guidance.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given an output schema exists, the description needn't document returns, and it covers everything else an agent needs: prerequisite actionId, required user confirmation, error behavior, and reversibility. No meaningful gap remains for invoking this tool correctly.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100% for the single actionId parameter, and the description largely restates the schema's own wording ('after the user confirmed it'). Baseline 3 applies since the schema already carries the semantics; the description adds only the terminal-confirmation nuance.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb and resource ('Saves a job on LinkedIn') with an execution tag up front. It clearly distinguishes itself from the sibling prepare/unsave tools by naming both, so an agent can route without opening any schema.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Explicitly states the precondition (an actionId from linkedin_prepare_save_job that the USER confirmed in their own terminal), the failure path (CONFIRMATION_REQUIRED), and the anti-pattern (do not retry in a loop, ask the user). It names the reversal tool linkedin_unsave_job and even gives a policy guardrail against gratuitous saves.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

linkedin_search_jobsSearch LinkedIn jobsA
Read-onlyIdempotent

[READ - no LinkedIn state is changed] Searches LinkedIn jobs and returns at most 25 results (one page). Every result is recorded in local tracking and annotated with isNew / localStatus, so you can tell jobs the user has never seen from ones already seen, saved, rejected or applied to. Set onlyNew=true to return only never-before-seen jobs. Use LinkedIn job IDs (the id field) to refer to jobs. Do not call repeatedly in a loop.

ParametersJSON Schema
NameRequiredDescriptionDefault
onlyNewNoReturn only jobs this agent has never returned before (local dedupe).
keywordsYesSearch keywords, e.g. "React TypeScript".
locationNoCity/region/country, e.g. "Tel Aviv, Israel".
workTypeNo
easyApplyNoOnly jobs with LinkedIn Easy Apply.
maxResultsNo1-25. Default is small on purpose; this is not a scraper.
postedWithinNo
employmentTypeNo

Output Schema

ParametersJSON Schema
NameRequiredDescription
jobsYes
fetchedYesUnique jobs LinkedIn returned before local filtering.
newCountYes
filteredOutYesJobs hidden because onlyNew=true and they were seen before.

TDQS

A4.2/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already cover safety (readOnlyHint, destructiveHint=false) and openWorld/idempotent traits, but the description adds real value: the 25-result cap, the local tracking side effect, and the isNew/localStatus annotation of results. It does not explain the interaction between idempotentHint and the tracking-based dedupe, which is a minor gap.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Front-loads the '[READ - no LinkedIn state is changed]' tag, then packs scope, return annotations, the onlyNew switch, ID convention, and the loop warning into three tight sentences with no filler.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

With an output schema present, return values need not be described, yet the definition still explains the isNew/localStatus annotations the agent will see. For an 8-parameter search tool at 63% schema coverage it is largely complete, with only the unmentioned filter parameters left to the schema.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 63%, so the schema documents most parameters already. The description adds meaningful semantics for onlyNew and reinforces the maxResults cap and job-ID convention, but says nothing about location, workType, easyApply, postedWithin, or employmentType beyond what the schema provides.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a precise verb+resource ('Searches LinkedIn jobs'), quantifies the scope ('at most 25 results (one page)'), and clarifies the read-only posture. An agent can distinguish it from linkedin_list_tracked_jobs and linkedin_get_job without opening their schemas.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Gives clear operating guidance: set onlyNew=true for unseen jobs, use the `id` field to refer to jobs, and 'Do not call repeatedly in a loop.' It lacks an explicit named alternative (e.g. list_tracked_jobs for re-checking already-seen jobs), which keeps it short of a 5.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

linkedin_unsave_jobUnsave a job on LinkedInA

[EXECUTE - external action; REQUIRES the user to have confirmed this exact action] Removes a job from the saved list on LinkedIn (changes LinkedIn state). Requires an actionId from linkedin_prepare_save_job (mode "unsave") that the USER has confirmed in their own terminal.

ParametersJSON Schema
NameRequiredDescriptionDefault
actionIdYesThe actionId returned by the matching prepare tool, after the user confirmed it.

Output Schema

ParametersJSON Schema
NameRequiredDescription
dataNo
actionYes
summaryYes

TDQS

A4.8/5.0
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already declare readOnlyHint=false, openWorldHint=true, and destructiveHint=false; the description adds the crucial external-side-effect context ('changes LinkedIn state'), the terminal-confirmation requirement, and the execute-stage guard. This goes well beyond what the structured annotations convey.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Front-loaded with the execute marker and the core action, and stays to two sentences. Slight redundancy between 'REQUIRES the user to have confirmed this exact action' and 'that the USER has confirmed in their own terminal' costs it the top mark.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a single-parameter execution tool with an output schema, everything an agent needs is present: the state-changing nature, the confirmation gate, and the source of the required actionId. No return-value explanation is needed since an output schema exists.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100%, so the actionId definition is already documented. The description adds provenance meaning (must come from the matching prepare tool, post-confirmation), which is useful but largely mirrors the schema text.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

States a specific verb and resource: 'Removes a job from the saved list on LinkedIn'. This clearly distinguishes it from linkedin_save_job and linkedin_prepare_save_job, and the '[EXECUTE]' framing signals the execution stage versus the prepare stage.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

Explicitly names the prerequisite chain: the actionId must come from linkedin_prepare_save_job (mode "unsave") and must have been confirmed by the user in their own terminal. An agent knows both which sibling to call first and the gate condition for invoking this one.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

  1. 20 tool updatesv0.1.0
    • First observedlinkedin_auth_status
    • First observedlinkedin_cancel_action
    • First observedlinkedin_draft_message
    • First observedlinkedin_get_action
    • First observedlinkedin_get_application
    • First observedlinkedin_get_company
    • First observedlinkedin_get_job
    • First observedlinkedin_get_profile
    • First observedlinkedin_get_saved_jobs
    • First observedlinkedin_list_actions
    • First observedlinkedin_list_tracked_jobs
    • First observedlinkedin_mark_job
    • First observedlinkedin_prepare_connection_request
    • First observedlinkedin_prepare_profile_update
    • First observedlinkedin_prepare_save_job
    • First observedlinkedin_preview_application
    • First observedlinkedin_provide_application_answer
    • First observedlinkedin_save_job
    • First observedlinkedin_search_jobs
    • First observedlinkedin_unsave_job

TDQS

A4.1/5.0

Scored across 20 tools

Disambiguation5/5

Each tool targets a distinct resource and action, with clear READ/PREPARE/EXECUTE labels and local-vs-LinkedIn boundaries. Related pairs like get_action/list_actions and get_application/preview_application are differentiated by singular-vs-list and structured-state-vs-rendered-preview purposes.

Naming Consistency5/5

All tools use the same linkedin_ prefix and snake_case convention, mostly following a verb_noun pattern. Minor variations like linkedin_auth_status remain readable and do not break the overall consistency.

Tool Count3/5

At 20 tools, the set is on the heavy side and falls into the borderline 16-25 range. While many tools earn their place through the prepare/execute safety model, some pairs could potentially be consolidated.

Completeness3/5

Core job search, tracking, job detail, and application preparation are covered. However, execute tools are missing for application submission, message sending, connection request sending, and profile update application, creating notable dead ends for end-to-end workflows.

Maintenance

ActivityMaintained
ResponsivenessNo issues

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    C
    maintenance
    A Model Context Protocol (MCP) server that provides tools to interact with LinkedIn's Feeds and Job API. You can do "search for 3 data engineer jobs in . For each job check if it a good match for me by analyzing it against my resume in file resume.md."
    207
    -
  • A
    license
    Not graded
    quality
    D
    maintenance
    Enables interaction with LinkedIn tools and services through the Universal MCP framework, allowing operations like posting and profile management via natural language.
    1
    MIT
  • A
    license
    Not graded
    quality
    B
    maintenance
    Enables natural language interaction with LinkedIn, including profile retrieval, job searching, messaging, and post engagement.
    3
    Apache 2.0
  • A
    license
    A
    quality
    D
    maintenance
    Enables searching and scraping of LinkedIn profiles, companies, jobs, and posts using natural language through MCP-compatible AI clients.
    13
    MIT