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:
find_jobswithaction: "get"to resolve a posting and read its apply gates, includingconnects_costandconnects_balancelist_freelancer_proposalsto check you have not already been invited, because creating a proposal over an existing invitation is rejectedmanage_proposalswithaction: "create"to open a draft with your cover letter and bidupdate_draftto attach answers to screening questions, which rotates the draft idconfirm_draftto 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 MCP | UpHunt MCP | |
|---|---|---|
| What it is for | Acting on a specific job or contract | Finding which jobs are worth acting on |
| Auth | OAuth to your Upwork account | Your UpHunt API key |
| Writes to Upwork | Yes, submits proposals | No, read-only |
| Continuous feed | Not permitted (Section 4.1) | Yes, that is its purpose |
| Scoring | None | AI score and a plain-language reason per job |
| Client history | Per contract | Spend, hire rate, open jobs before you bid |
| Runs | Hosted by Upwork | Locally, 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 scoreget_job— full details for any job by URL or ciphertext, including budget, skills, and screening questionsget_client— a buyer's open jobs, hire history, and spend statslist_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 withuphunt.get_clientbefore you spend anything. Draft and submit with Upwork'smanage_proposalsandconfirm_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.