I first put my LaTeX resume on GitHub because people kept asking me for the template. Before that, I was literally setting up Overleaf accounts for people so they could use it. Publishing the repo was much easier.

I have not updated the public version in years and probably will not, but my local version kept growing anyway.

It picked up tailored resumes, application tracking, interview prep, recruiter notes, and detailed notes about my projects. Eventually it became the place where I run most of my job search. Agents can compare a role with my actual experience, decide if it is worth applying, tailor the resume, and prepare for an interview without asking me to explain my background again every time.

This worked well until the agent had to answer one question.

Should I spend time applying to this role if I need H-1B sponsorship?

A generic company profile is not much help here, and neither is a vague memory that an employer “probably sponsors.” I needed evidence. Has the company filed LCAs recently? Are there USCIS approvals? Has it sponsored similar job titles? What do the salary rows say? Does the city have enough sponsorship activity to make the role worth investigating?

h1b-mcp is the small MCP server I built to answer those questions inside an agent workflow. It connects to H1BGrader, fetches the relevant profile through a real browser, parses the tables into typed models, caches the result locally, and exposes focused MCP tools that an agent can use while screening roles.

It cannot guarantee that a particular job will sponsor, and it is not legal advice. It gives the agent enough structured evidence to recommend apply, skip, or investigate further.

Why MCP Was the Right Shape

The first version of this idea could have been a standalone CLI command.

h1b-check databricks

That is fine for an occasional manual check. It is awkward when an agent is already reading the job description, comparing it with my project notes, and deciding whether tailoring is worth the time. Leaving that loop to run another command and interpret its output puts the integration work back on me.

Sponsorship checking belongs inside that loop. If the agent is already deciding whether a role is worth time, it should be able to ask:

  • does this company sponsor H-1B workers?
  • does this company sponsor roles close to this title?
  • which employers sponsor this role in general?
  • what are the LCA, USCIS, salary, and grade signals behind the answer?

MCP made that interface natural. Instead of designing a chat command or a custom API for one client, the project exposes tools with typed input and output schemas. A compatible client can inspect those schemas through tools/list, call the right tool, and receive structured content that the model can synthesize into a human answer.

The Architecture

The live fetch is expensive. Once a profile has been parsed, answering questions from it is cheap. A company profile can cover sponsorship signal, role match, salaries, LCA and USCIS trends, grade, and job-title history. Job-title and city profiles work the same way.

So the server separates profile fetching from tool responses.

flowchart LR
    A[MCP tool call] --> B[FastMCP handler]
    B --> C[Shared tool runner]
    C --> D[H1BGraderService]

    D --> E{Profile type}
    E --> F[CompanyProfile cache]
    E --> G[JobTitleProfile cache]
    E --> H[LocationProfile cache]

    F --> I{Fresh cache hit?}
    G --> I
    H --> I

    I -->|yes| J[Load typed profile]
    I -->|no| K[H1BGraderClient]

    K --> L[SeleniumBase UC browser]
    L --> M[H1BGrader typeahead search]
    M --> N[Open required tabs]
    N --> O[Parse page source]
    O --> P[Write typed profile to cache]

    J --> Q[Response builders]
    P --> Q

    Q --> R[Focused MCP result]

The modules mirror that split:

  • h1b_mcp.h1bgrader owns the browser-backed lookup flow.
  • h1b_mcp.parsers converts H1BGrader HTML into structured profile data.
  • h1b_mcp.cache.ProfileCache stores parsed profiles by normalized aliases.
  • h1b_mcp.service.H1BGraderService coordinates cache reads, live fetches, and writes.
  • h1b_mcp.responses turns cached profile models into focused tool responses.
  • h1b_mcp.tools registers the FastMCP tools for companies, job titles, and locations.

The tools do not scrape the website separately. They share one parsed profile and select the part that answers the current question.

The Browser Was the Hard Part

H1BGrader looks simple from the outside. Search for a company, open its profile, and read a few tables. Automating that flow took more work than the MCP server itself.

The first implementation used Playwright. It was the obvious place to start and the API was good, but the first live probe opened a Cloudflare page with Just a moment... instead of H1BGrader. Running a visible browser and keeping a profile did not make the automated session reliable.

I switched to Patchright next. Since it keeps the Playwright API, the first change was almost just an import swap.

# first attemptfrom playwright.async_api import async_playwright# second attemptfrom patchright.async_api import async_playwright

Patchright got us closer, but it did not clear the H1BGrader challenge consistently. We tried a persistent Chrome profile, opened a normal Chrome window for manual verification, and reused the saved cf_clearance cookie on the next lookup. The cookie was present and the automated session still got challenged. Leaving the verification window open also locked the profile and stopped the next browser from starting.

I tried regular Selenium after that, followed by SeleniumBase. Plain SeleniumBase still behaved like a normal automated browser. The version that finally worked was SeleniumBase with UC mode.

The final session setup is small. This is the code used by HomepageSession, with the surrounding cleanup method omitted.

def __enter__(self) -> Any:    self._settings.profile_dir.mkdir(parents=True, exist_ok=True)    self._sb_context = SB(        browser="chrome",        uc=True,        headless=True,        user_data_dir=str(self._settings.profile_dir),        timeout_multiplier=seleniumbase_timeout_multiplier(self._settings.timeout_ms),    )    sb = self._sb_context.__enter__()    try:        self._open_homepage(sb)    except BaseException:        self._sb_context.__exit__(*sys.exc_info())        raise    return sbdef _open_homepage(self, sb: Any) -> None:    self._logger.info("Opening H1BGrader homepage.")    sb.uc_open_with_reconnect(self._settings.base_url, reconnect_time=6)    sb.sleep(2)    raise_if_challenged(sb)    self._logger.info("H1BGrader homepage loaded.")

uc=True enables SeleniumBase’s UC mode, while uc_open_with_reconnect opens the page and reconnects after the initial browser handshake. That combination was able to get through the Cloudflare gate without bringing back the manual verification loop.

Getting through the homepage was only the first step. H1BGrader uses typeahead search and renders some tables only after their tabs have been opened. Every lookup still has to run the full browser flow:

  1. Open the H1BGrader homepage.
  2. Select the company, job title, or city search tab.
  3. Type the query into the shared typeahead input.
  4. Click the selected suggestion.
  5. Open the profile tabs needed for that entity.
  6. Parse the resulting page source.

Company, job-title, and location pages use the same navigation code. The parts that change live in a typed lookup spec.

@dataclass(frozen=True)class H1BGraderLookupSpec(Generic[TProfile]):    field_name: str    search_type: str    label: str    path_pattern: Pattern[str]    parse: Callable[[str, str | None, str, Any], TProfile]    tabs_to_open: tuple[tuple[str, str], ...] = field(default_factory=tuple)COMPANY_LOOKUP_SPEC: H1BGraderLookupSpec[CompanyProfile] = H1BGraderLookupSpec(    field_name="company_name",    search_type="company",    label="company",    path_pattern=COMPANY_PROFILE_PATH_RE,    parse=_parse_company_profile,    tabs_to_open=(        (COMPANY_SALARIES_TAB_SELECTOR, "salaries"),        (COMPANY_GRADE_TAB_SELECTOR, "grade"),        (COMPANY_JOB_TITLES_TAB_SELECTOR, "job titles"),    ),)

The browser client takes a query and a lookup spec, then returns a typed profile. It does not need separate search implementations for every page, and the rest of the server does not need to know which selector opens the salary tab.

Parsed Profiles, Not One-Off Answers

The cache stores source-backed profile objects rather than final answers.

For companies, the profile contains:

  • LCA approval and denial counts
  • LCA approval and denial rates
  • USCIS approval and denial counts
  • USCIS approval and denial rates
  • H-1B salary rows
  • sponsored job titles
  • H1BGrader grade cards and grade insights

For job titles and locations, the aggregate profile contains:

  • LCA trends
  • approval-rate rows
  • salary history
  • top employers

Tool responses are then just projections over those profiles. For example, get_company_uscis_trends returns the USCIS rows, while check_company_job_sponsorship uses the same company profile but runs a local job-title matcher over the sponsored title table.

One Databricks lookup can then support a chain of agent questions:

  • is Databricks a sponsor?
  • does it sponsor data science roles?
  • what are the latest USCIS approvals?
  • what titles has it filed LCAs for?
  • what salary ranges show up in the H-1B data?

The expensive part happens once. The focused answers are cheap.

Tool Design

The tool surface is grouped by entity:

Company tools

  • sponsorship signal
  • company + job-title sponsorship context
  • full company profile
  • job titles
  • LCA trends
  • USCIS trends
  • salaries
  • grade

Job-title tools

  • sponsorship signal across employers
  • full job-title profile
  • top employers
  • LCA trends
  • salary history

Location tools

  • sponsorship signal across employers in a city
  • full city profile
  • top employers
  • LCA trends
  • salary history

The docstrings are written from the agent’s perspective, not as internal Python documentation. A tool description like “Use to inspect an employer’s annual USCIS approval and denial history” is more useful to an agent than “returns CompanyUSCISTrendsResult.” The schema already tells the client the return type. The description should help decide when to use the tool.

Failure Behavior

The server avoids pretending when H1BGrader blocks lookup or human verification appears. Browser blocks, missing typeahead suggestions, and unexpected navigation failures surface as MCP tool errors with clear messages.

A blocked lookup cannot quietly become “no sponsorship evidence.” Those are different states. The server returns parsed evidence or tells the caller that it could not fetch the data reliably.

Packaging and Usage

The project is published on PyPI and can run through uvx.

Install uv first so uvx is available.

uvx h1b-mcp@latest

For Codex, the MCP config looks like this.

[mcp_servers.h1b]command = "uvx"args = ["h1b-mcp@latest"]env = { UV_HTTP_TIMEOUT = "300" }tool_timeout_sec = 300

The timeout is not decorative. Browser-backed lookups can take longer than a plain API call, especially on the first run when SeleniumBase prepares the browser driver.

The Tool in Action

This is a real lookup from Codex asking whether Databricks sponsors Data Scientists.

Codex using h1b-mcp to check Databricks sponsorship for a Data Scientist role

The agent calls get_company_role_sponsorship_context with the company and job title. The tool returns the company-level signal, recent LCA and USCIS evidence, the closest sponsored title, and salary data. Codex then turns that payload into an apply recommendation while keeping the individual job description as the final check.

What This Enables

Behind MCP, the sponsorship check becomes part of the larger application decision. The agent can read a role, check the company and title, then decide whether the evidence is strong enough to justify tailoring.

An answer can be more nuanced than “yes” or “no.”

Databricks has strong company-level H-1B evidence. For Data Scientist specifically, the closest company title match is Sr Data Scientist with LCA activity, and the broader company profile shows recent LCA and USCIS approval rows. Sponsorship is not the blocker, but still check the individual job description for role-specific language.

This was the missing half of the decision. The resume repo gives the agent my background, projects, and the roles I want. h1b-mcp tells it whether an opportunity has enough sponsorship evidence to be worth my time.

I still read the posting carefully. I just reach it with better evidence now.