MCP connector

Connect Claude Code, OpenClaw, or any MCP client to your own job.os data.

job.os exposes an MCP server at https://jobs.hemnaath.tech/mcp, gated by standard MCP OAuth: the same discovery-and-consent flow Atlassian’s or Figma’s connectors use, and the same one the MCP Authorization spec defines. Nothing here is Claude-specific under the hood: any client that speaks that spec can discover the server at /.well-known/oauth-authorization-server and /.well-known/oauth-protected-resource/mcp, run the OAuth flow, and call the same tools. You connect once, sign in, approve a consent screen, and the client acts as you from then on, scoped to your account only.

Connect from Claude Code

claude mcp add --transport http job-os https://jobs.hemnaath.tech/mcp

Then, in a real terminal (not a non-interactive shell), run:

claude mcp login job-os

That opens your browser to sign in and approve the consent screen. Once it confirms, run /mcp inside any Claude Code session to see job-os connected.

Connect from any MCP client

Point any MCP-compliant client at the streamable-HTTP endpoint below and let it drive OAuth discovery itself:

https://jobs.hemnaath.tech/mcp

As one concrete example, OpenClaw registers an outbound MCP server and its OAuth credentials with:

openclaw mcp add job-os \
  --url https://jobs.hemnaath.tech/mcp \
  --transport streamable-http \
  --auth oauth

openclaw mcp login job-os

A model on its own, such as one of the Hermes releases, does not connect to an MCP server directly: the agent harness running that model is the actual MCP client, and whichever harness you use, the same URL and the same OAuth flow apply. If your client’s connect flow needs a name for it, job-os is as good as any; nothing about the server cares what the client calls it.

How the OAuth flow works

You do not need any of this to connect a compliant client, which handles all of it. It matters if you are writing your own client, or working out why one is failing.

  1. Call POST /mcp with no token. It answers 401 with a WWW-Authenticate header naming the metadata document.
  2. Fetch /.well-known/oauth-protected-resource/mcp. It names the authorization server, clerk.jobs.hemnaath.tech, and the scopes to ask for.
  3. POST to that server’s /oauth/register. Dynamic client registration is open, so this needs no credentials and returns a client_id.
  4. Send the person to /oauth/authorize with PKCE (S256). They sign in and approve a consent screen.
  5. Exchange the code at /oauth/token. Public clients may exchange without a client_secret, so a CLI or desktop client needs no secret of its own.
  6. Send the token as Authorization: Bearer on every call.

The scopes requested are profile, email and offline_access, and no more. The first two say who you are; every tool scopes its reads and writes to that person and nothing else. The third is what keeps the connection alive: an access token expires after a day and a refresh token never does, so without it an unattended agent would stop working every 24 hours and need someone back at a browser. public_metadata and private_metadata are deliberately not requested, because no tool here reads them.

A human has to sign in once, per client. There is no machine-to-machine path: only the authorization-code and refresh-token grants exist, and the server accepts nothing but an OAuth access token, so there are no API keys to issue. That is the point rather than a limitation. The token is issued to you, and an agent holding it can reach your data and no one else’s.

When the connection fails

Almost every failure lands your browser on a /callback URL carrying an error and an error_description. Read that description: it is far more specific than whatever the client prints.

invalid_scope means the client asked for something it is not allowed. The usual cause is a client that requests openid out of habit, since most OpenID Connect clients always do. This server does not advertise it, because the authorization server grants no client that scope, and asking for it fails only after you have signed in. If your client insists on sending it, the fix is on the Clerk side: assign openid to the instance’s default scopes, then re-register the client, since an existing registration keeps the scopes it was issued with.

Worth knowing if you are debugging your own client: the authorization server’s scopes_supported lists what the instance supports, which is wider than what any client may actually request. The scope returned by /oauth/register is the real list.

A failed attempt leaves a registered client cached, so clear it before retrying, or the client will repeat the request that failed. In Claude Code that is claude mcp logout job-os; elsewhere, remove and re-add the connection.

Agent instructions

Once connected, an agent still has to know how to use the tools well. Drop this into its system prompt, its AGENTS.md, or wherever it reads standing instructions from:

You have access to job.os, a personal job-search platform, through MCP. Use it to search for roles, track applications, and draft tailored resumes for the person who authorized this connection -- every tool call is scoped to their account alone.

Typical workflow:
1. Find postings with search_jobs (a pre-built index crawled overnight, not a live board fetch, so it answers fast), or add a specific posting the person gives you with add_job_from_url / add_job_from_text. Pass status to also create the pipeline entry in the same call.
2. Check list_resumes for their existing resumes (there is usually a master and a handful of company-specific ones). Use get_profile_facts to see what is actually verified about them -- never claim experience, a metric, or a technology that is not backed by a verified fact. If a job wants something the facts do not support, say so instead of inventing it, or capture it with create_profile_fact first (see the rule on verified below).
3. Call start_resume_tailor(resume_id, job_id) to draft a resume tailored to one job. It returns immediately with an agent_job_id; it does not wait for the draft to finish.
4. Poll get_resume_tailor_status(agent_job_id) every few seconds until status is "succeeded" or "failed". You can start several tailor runs back to back, for different jobs or different resumes, and poll each independently -- they run as genuinely concurrent agent jobs, not one after another.
5. The result of a succeeded job is a draft resume version, an id you get from that response. To make it final: call start_resume_finalize(version_id) for a job_id, then poll get_resume_finalize_status(version_id, job_id) the same way. Once done, the version is either finalized (the review passed) or blocked (it did not; the review is attached so you can see why, and you can call the status tool again with force: true to finalize anyway).
6. Once a version is finalized, download_resume_version(version_id) returns the actual PDF, named after the candidate, company, and role it was tailored for rather than the raw version id. It answers { status: "not_ready" } instead of a file if the version has no rendered PDF yet.
7. Use list_applications / get_application_timeline to check pipeline status, and update_application_status to move something forward once the person confirms.

Rules:
- Nothing here is destructive. archive_resume, move_resume_version, and archive_profile_fact never delete data, only hide or relocate it.
- update_profile_fact corrects a fact's fields, most usefully payload.keywords, which is what decides how a project ranks against a posting: a project with no technologies listed cannot match a job description written in technology nouns, however relevant it is. It cannot set verified.
- patch_bullet changes one bullet's wording, for tightening a bullet past 30 words or varying a repeated opening verb. It refuses an edit that introduces a number the fact does not already state, or that grows the bullet past its current length or the cap. Those are the rules the tailor is held to, and they bind harder here: a tailored resume is one document, an edit to the vault is inherited by every resume after it. If a bullet needs a metric it does not have, ask for it.
- create_profile_fact defaults verified to false. Only pass verified: true when the person has explicitly confirmed the fact themselves in this conversation: an unverified fact is never cited in a tailored resume, and a wrongly-verified one corrupts every resume tailored after it.
- If a tool call fails with "not found" for an id you were given, do not retry with a guessed id -- ask instead.
- Report what you actually did (which tools, which ids) rather than a generic "I tailored your resume," so the person can verify it in the web app.

What’s exposed

Thirty-one tools, covering jobs, applications, resume tailoring, the resume library, resume files, profile facts, cover letters, and calendar. Most are pure reads; the writes are additive or reversible, nothing destructive, and nothing here can touch another user’s data. start_resume_tailor/get_resume_tailor_status and start_resume_finalize/get_resume_finalize_status are both start/poll pairs: the first call returns a job id immediately instead of blocking on work that takes real time, and each job runs independently, so several run concurrently rather than queueing behind each other.

Both add_job_from_url and add_job_from_text take an optional status to create the pipeline entry in the same call, matching the web app’s “Add to wishlist” button. upload_resume_version takes either content_base64 (inline bytes) or source_url (an https URL job.os fetches itself, same pattern as add_job_from_url): use source_url for anything beyond a trivially small file, since inlining a real PDF as base64 can be too large to reliably round-trip through a model’s own context.

Jobs

list_jobs, get_job, add_job_from_url, add_job_from_text, search_jobs

Applications

list_applications, get_application, get_application_timeline, create_application, update_application_status, sync_application_to_appwrite

Resume tailoring

start_resume_tailor, get_resume_tailor_status, start_resume_finalize, get_resume_finalize_status, download_resume_version

Resume library

list_resumes, list_appwrite_resumes, create_resume, archive_resume, move_resume_version, get_application_resume_versions

Resume files

upload_resume_version, create_resume_upload_url, confirm_resume_upload, sync_resume_version_to_appwrite, resync_resume_to_appwrite

Other

whoami, get_profile_facts, create_profile_fact, update_profile_fact, patch_bullet, archive_profile_fact, list_cover_letters, get_upcoming_calendar

Add it for your own users

Anyone can add job.os as a plugin marketplace, no client credentials needed:

/plugin marketplace add hemnaath04/job-os
/plugin install job-os@job-os