update_profile
Update Upwork freelancer profile fields including title, description, hourly rate, and skills. Specify only the fields you want to change; requires browser automation.
Instructions
Update your Upwork freelancer profile fields. Can update: title, description/bio, hourly_rate, skills. Each field is optional — only provide what you want to change. Uses the edit buttons on your profile page (requires browser via connect-chrome.bat).
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| title | No | Professional title shown on your profile | |
| description | No | Profile overview/bio | |
| hourly_rate | No | Hourly rate in USD | |
| skills | No | Skills to add |
Implementation Reference
- src/tools/update-profile.ts:4-10 (schema)Zod schema (UpdateProfileSchema) defining optional fields: title, description, hourly_rate, skills — and the inferred TypeScript type UpdateProfileInput.
export const UpdateProfileSchema = z.object({ title: z.string().optional().describe('Professional title, e.g. "n8n Workflow Automation Expert"'), description: z.string().optional().describe('Profile overview/bio text'), hourly_rate: z.coerce.number().optional().describe('Hourly rate in USD'), skills: z.array(z.string()).optional().describe('List of skills to add'), }); - src/tools/update-profile.ts:98-229 (handler)Exported async function updateProfile — the core handler. It authenticates via ensureLoggedIn, navigates to the profile page, then edits title/hourly_rate/description/skills using Playwright automation (clicking edit buttons, filling fields, saving modals). Returns success, updated, and errors.
export async function updateProfile(input: UpdateProfileInput): Promise<{ success: boolean; updated: string[]; errors: string[]; }> { const page = await ensureLoggedIn(); const updated: string[] = []; const errors: string[] = []; try { const profileUrl = await getProfileUrl(page); log('Profile URL:', profileUrl); await page.goto(profileUrl, { waitUntil: 'domcontentloaded', timeout: 30000 }); await humanDelay(3000, 4000); // ── Update Title ────────────────────────────────────────────────── if (input.title) { log('Updating title...'); if (await clickEditButton(page, 'Edit title')) { await waitForModal(page); const filled = await fillField(page, 'input[name*="title"], input[placeholder*="title"], input[id*="title"]', input.title) || await fillField(page, 'input[type="text"]', input.title); if (filled && await saveModal(page)) { updated.push(`title: "${input.title}"`); log('Title updated.'); } else { errors.push('title: could not fill/save'); } } else { errors.push('title: edit button not found'); } } // ── Update Hourly Rate ──────────────────────────────────────────── if (input.hourly_rate) { log('Updating hourly rate...'); if (await clickEditButton(page, 'Edit hourly rate')) { await waitForModal(page); const filled = await fillField(page, 'input[name*="rate"], input[placeholder*="rate"], input[type="number"]', String(input.hourly_rate)); if (filled && await saveModal(page)) { updated.push(`hourly_rate: $${input.hourly_rate}`); log('Hourly rate updated.'); } else { errors.push('hourly_rate: could not fill/save'); } } else { errors.push('hourly_rate: edit button not found'); } } // ── Update Description / Overview ──────────────────────────────── if (input.description) { log('Updating description...'); const descBtnLabels = ['Edit overview', 'Edit description', 'Edit bio', 'Edit professional overview', 'Edit summary']; let descOpened = false; for (const label of descBtnLabels) { if (await clickEditButton(page, label)) { descOpened = true; break; } } if (descOpened) { await waitForModal(page); // Try known textarea selectors (Upwork uses #profile-description) let filled = await fillField(page, '#profile-description', input.description) || await fillField(page, 'textarea[id*="description"], textarea[id*="overview"], textarea[id*="bio"]', input.description) || await fillField(page, 'textarea', input.description); if (!filled) { // Contenteditable: select all + type try { const editor = page.locator('[contenteditable="true"]').first(); await editor.waitFor({ state: 'visible', timeout: 5000 }); await editor.click(); await page.keyboard.press('Control+a'); await page.keyboard.type(input.description, { delay: 10 }); filled = true; } catch { /* noop */ } } if (filled && await saveModal(page)) { updated.push('description updated'); log('Description updated.'); } else { errors.push('description: could not fill/save'); } } else { errors.push('description: edit button not found (tried: ' + descBtnLabels.join(', ') + ')'); } } // ── Update Skills ───────────────────────────────────────────────── if (input.skills?.length) { log('Updating skills...'); if (await clickEditButton(page, 'Edit skills')) { await waitForModal(page); for (const skill of input.skills) { try { // Type into skills search input const skillInput = page.locator('input[placeholder*="skill"], input[placeholder*="Search"]').first(); await skillInput.waitFor({ state: 'visible', timeout: 5000 }); await skillInput.fill(skill); await humanDelay(800, 1200); // Click first suggestion const suggestion = page.locator('[class*="suggestion"], [class*="option"], [role="option"]').first(); const hasSuggestion = await suggestion.isVisible({ timeout: 3000 }).catch(() => false); if (hasSuggestion) { await suggestion.click(); await humanDelay(400, 700); updated.push(`skill: "${skill}"`); } else { // Try pressing Enter await skillInput.press('Enter'); await humanDelay(400, 700); updated.push(`skill: "${skill}" (enter)`); } } catch (e) { errors.push(`skill "${skill}": ${e instanceof Error ? e.message : e}`); } } if (await saveModal(page)) { log('Skills saved.'); } else { errors.push('skills: could not save'); } } else { errors.push('skills: edit button not found'); } } return { success: errors.length === 0, updated, errors }; } finally { await page.close(); } } - src/tools/update-profile.ts:15-96 (helper)Internal helpers used by updateProfile: getProfileUrl, clickEditButton, waitForModal, fillField, saveModal — all Playwright-based utilities.
async function getProfileUrl(page: Page): Promise<string> { await page.goto('https://www.upwork.com/freelancers/settings/profile', { waitUntil: 'domcontentloaded', timeout: 30000, }); await humanDelay(1500, 2500); const href = await page.evaluate(() => document.querySelector('a[href*="/freelancers/~"]')?.getAttribute('href') ?? '' ); if (!href) throw new Error('Could not find profile URL'); return href.startsWith('http') ? href : `https://www.upwork.com${href}`; } /** Click an edit button by aria-label, wait for modal/form to appear */ async function clickEditButton(page: Page, ariaLabel: string): Promise<boolean> { try { const btn = page.locator(`button[aria-label="${ariaLabel}"]`).first(); await btn.waitFor({ state: 'visible', timeout: 5000 }); await btn.click(); await humanDelay(800, 1500); return true; } catch { log(`Edit button "${ariaLabel}" not found`); return false; } } /** Wait for a modal/drawer to appear */ async function waitForModal(page: Page): Promise<void> { await page.waitForSelector( '[role="dialog"], [class*="modal"], [class*="drawer"], [class*="slide-panel"]', { state: 'visible', timeout: 8000 } ).catch(() => log('Modal not detected, continuing anyway')); await humanDelay(500, 1000); } /** Clear and fill a text input/textarea — uses pressSequentially to trigger React events */ async function fillField(page: Page, selector: string, value: string): Promise<boolean> { try { const el = page.locator(selector).first(); await el.waitFor({ state: 'visible', timeout: 5000 }); await el.click(); await page.keyboard.press('Control+a'); await page.keyboard.press('Delete'); await humanDelay(100, 200); // pressSequentially fires keydown/keypress/input/keyup — React picks these up await el.pressSequentially(value, { delay: 8 }); await humanDelay(300, 500); return true; } catch { return false; } } /** Click Save/Submit button in a modal — waits for it to become enabled (React validation) */ async function saveModal(page: Page): Promise<boolean> { const saveSelectors = [ 'button[type="submit"]', 'button:has-text("Save")', 'button:has-text("Apply")', 'button:has-text("Done")', '[data-test="save-btn"]', ]; for (const sel of saveSelectors) { try { const btn = page.locator(sel).first(); const visible = await btn.isVisible({ timeout: 2000 }).catch(() => false); if (!visible) continue; // Wait for button to be enabled (React re-enables after valid input) await btn.waitFor({ state: 'visible', timeout: 5000 }); // Poll for enabled state up to 5s for (let i = 0; i < 10; i++) { const disabled = await btn.isDisabled().catch(() => true); if (!disabled) break; await humanDelay(500, 600); } await btn.click({ force: true }); // force=true to click even if briefly disabled await humanDelay(1000, 2000); return true; } catch { /* try next */ } } return false; } - src/browser/upwork-auth.ts:11-23 (helper)ensureLoggedIn helper that establishes a logged-in Playwright Page (via CDP or saved session). Called by updateProfile before automation.
export async function ensureLoggedIn(): Promise<Page> { await browserManager.init(); const page = await browserManager.newPage(); log('Browser ready (CDP mode)'); // Quick check — navigate home if page is blank if (!page.url() || page.url() === 'about:blank') { await page.goto('https://www.upwork.com', { waitUntil: 'domcontentloaded', timeout: 15000 }); } return page; } - src/index.ts:41-55 (registration)MCP tool registration (index.ts) — defines the tool name, description, and inputSchema properties in the TOOLS array. Also the dispatch in the CallToolRequestSchema handler at lines 288-291.
name: 'update_profile', description: `Update your Upwork freelancer profile fields. Can update: title, description/bio, hourly_rate, skills. Each field is optional — only provide what you want to change. Uses the edit buttons on your profile page (requires browser via connect-chrome.bat).`, inputSchema: { type: 'object', properties: { title: { type: 'string', description: 'Professional title shown on your profile' }, description: { type: 'string', description: 'Profile overview/bio' }, hourly_rate: { type: ['number', 'string'], description: 'Hourly rate in USD' }, skills: { type: 'array', items: { type: 'string' }, description: 'Skills to add' }, }, }, },