UpHunt - AI-Powered Freelance Job Platform
UpHunt
Back to Blog
about 19 hours ago·7 min read

UpHunt Team

Share

Upwork's MCP Can Submit Proposals. It Can't Build You a Job Feed.

Upwork shipped an official MCP server at https://mcp.upwork.com/mcp. Connect it to Claude, Cursor, Cline, or any MCP client, authorize over OAuth, and your agent gets 46 tools prefixed upwork__. It can read your dashboard, list contracts, send messages, and yes, submit proposals end to end without touching a browser.

If you have wired it up already, you have probably hit the same wall everyone does: submitting is the easy half. Finding the right jobs to submit to is the hard half. And that gap is not an oversight or a missing feature. It is written into Upwork's terms on purpose.

What Upwork's MCP does well

The apply path is genuinely complete. A working sequence looks like this:

  1. find_jobs with action: "get" to resolve a posting and read its apply gates, including connects_cost and connects_balance
  2. list_freelancer_proposals to check you have not already been invited, because creating a proposal over an existing invitation is rejected
  3. manage_proposals with action: "create" to open a draft with your cover letter and bid
  4. update_draft to attach answers to screening questions, which rotates the draft id
  5. confirm_draft to submit

Connects are charged at confirm_draft, not at draft creation, so everything before the final step costs nothing. That makes the flow pleasant to develop against.

Auth is OAuth 2.1 with PKCE. Access tokens last 24 hours, refresh tokens rotate on every use, and reuse detection revokes the entire grant, not just the token. If you build on this, serialize your refreshes behind a single writer. Two workers refreshing the same connection concurrently will disconnect your user, and the failure is silent until the next call.

Where it stops: continuous discovery

find_jobs exists and it searches. So why can't you point it at a cron and build a feed?

Because Upwork's API & MCP Terms of Use say you cannot. Section 4.1, on purpose limitation, permits searching and browsing, then adds:

"For clarity, permission to search or browse Upwork Content authorizes only access reasonably necessary to perform a specific, documented, user-directed task and does not authorize activity designed to enumerate or continuously monitor Upwork's available content corpus."

That is the sentence. Polling find_jobs every few minutes to maintain a live index of open jobs is enumeration and continuous monitoring. It is not a rate limit you can negotiate around or a quota you can pay to raise. It is a scope boundary.

Section 7.2 closes the obvious workarounds explicitly, prohibiting "creating multiple Developer Applications for identical or substantially similar purposes; fanning load across multiple accounts, Credentials, or scope tokens; rotating IP addresses or headers to evade limits."

So the MCP is built for user-directed tasks: this user, this job, right now. It is not built to be your search index.

And rolling your own scraper is worse

The natural next thought is to scrape the job listings yourself and use the MCP only for applying. Section 5.2 anticipates that. It defines anything obtained through "scraping, crawling, spidering, headless browsing, web-browser plug-ins, optical character recognition ... multi-account fan-out, header spoofing, IP rotation, or any other technology used to access Upwork content outside the Tools" as Non-Official Content, and then prohibits two things in the same breath:

"access, store, display, transfer, or use any Upwork content obtained through [those techniques] ...; combine Upwork Content with Non-Official Content"

Read that second clause carefully if you are building a hybrid. Feeding a scraped job id into find_jobs get is combining the two in the most literal sense available. Once you have accepted the API & MCP Terms, and Section 1.1 says you accept them simply by calling a Tool, the scraper you were running before is no longer a separate concern sitting outside your agreement.

This is the part most teams building on the new MCP have not read yet, and it is the part that decides your architecture.

The shape of the answer: two MCPs, two jobs

Discovery and transaction are different problems with different constraints, so they want different servers.

Upwork MCPUpHunt MCP
What it is forActing on a specific job or contractFinding which jobs are worth acting on
AuthOAuth to your Upwork accountYour UpHunt API key
Writes to UpworkYes, submits proposalsNo, read-only
Continuous feedNot permitted (Section 4.1)Yes, that is its purpose
ScoringNoneAI score and a plain-language reason per job
Client historyPer contractSpend, hire rate, open jobs before you bid
RunsHosted by UpworkLocally, on your machine

UpHunt has shipped an MCP server since before Upwork announced theirs, and it does the half Upwork's deliberately does not: a live, scored, filterable job feed your agent can query as often as it likes, plus the client research you want before committing Connects.

Four tools, all read-only:

  • search_jobs — search your live AI-scored feed by keyword, budget, job type, and minimum match score
  • get_job — full details for any job by URL or ciphertext, including budget, skills, and screening questions
  • get_client — a buyer's open jobs, hire history, and spend stats
  • list_applied_jobs — what you have already applied to, with scores and current status

Running both together

Add UpHunt alongside Upwork in the same MCP config. Claude Desktop, Cursor, Cline, Windsurf, and Claude Code all work:

{
  "mcpServers": {
    "uphunt": {
      "command": "npx",
      "args": ["-y", "uphunt-mcp"],
      "env": {
        "UPHUNT_API_KEY": "your_api_key_here"
      }
    }
  }
}

Generate the key in your UpHunt dashboard under Auto-Apply, API and Webhooks, then restart your client.

With both connected, the division of labour your agent settles into looks like this:

Find with uphunt.search_jobs, filtering to a minimum score so you are reading ten jobs instead of four hundred. Research the buyer with uphunt.get_client before you spend anything. Draft and submit with Upwork's manage_proposals and confirm_draft, which is the only path that actually writes to Upwork.

Discovery stays continuous and cheap because it is running against a feed built for that. Submission stays sanctioned because it goes through Upwork's own API, under a grant your user authorized on Upwork's consent screen, and it is revocable there at any time.

Three things worth knowing before you build

Screening answers must be real. Upwork charges Connects at confirm time and rejects empty or placeholder answers afterward. Validate before you call confirm_draft, or you will pay for proposals that never land.

Error codes come back in SCREAMING_SNAKE_CASE. JOB_IS_NO_LONGER_AVAILABLE, INSUFFICIENT_CONNECTS, NOT_ELIGIBLE_FOR_JOB. If your error handling was written against prose messages, these will fall through to a generic failure and get retried, which is wasted work on a job that closed.

Terminal outcomes should not retry. A closed job, an ineligible account, and an empty Connects balance do not change in twenty seconds. Retrying them burns your rate limit and leaves the work looking stuck.

The short version

Upwork's MCP is the sanctioned way to submit proposals from an agent, and it is a real improvement over browser automation: no captchas, no session management, no risk to the account from driving a UI. Use it for that.

Just do not expect it to be your job search. Upwork wrote that boundary into Section 4.1 on purpose, and Section 5.2 closes the workaround. Pair it with a server built for discovery instead.

Try the UpHunt MCP server or start free and get your API key.

Try UpHunt

Stop scrolling Upwork all day.

UpHunt scores every job 1-10 and auto-applies with personalized proposals 24/7. Set it up in 5 minutes.

Start Free Trial

Related Posts