| save_noteA | Save a note as a markdown file in the local notes store. Returns the path of the file written. |
| list_notesB | List the most recently saved notes (filename + first line). |
| search_notesB | Full-text search across saved notes. Returns matching filenames. |
| read_sheet_rangeA | Read cell values from a Google Sheet. Read this before writing: it shows the header row and where the data
ends so a write can be column-mapped and placed without guessing. sheet_id is the spreadsheet's ID (the long token in its URL,
.../spreadsheets/d/<sheet_id>/edit); it is required, so confirm
which sheet to read per call. range is an A1 notation range — a bare
tab name like Sheet1 reads the whole used area, or pass something
like Sheet1!A1:E1 for just the header row. value_render_option
FORMATTED_VALUE returns values as displayed; UNFORMATTED_VALUE returns
raw numbers/dates; FORMULA returns the underlying formulas.
Returns {"range": <resolved A1 range>, "values": <rows>} where
values is a list of rows, each a list of cell strings. Trailing
empty rows and cells are omitted by the API, so len(values) is the
number of populated rows from the top of range — the next free row
when range starts at row 1 is len(values) + 1. |
| append_sheet_rowA | Append a single row directly below a tab's last populated row. Placement is deterministic: the tool reads the tab, takes the row after
the last one holding any value, and writes there starting at column A.
It does not use the Sheets API's table auto-detection, which misplaced
rows on real trackers (blank gap rows, a blank first cell, tab titles
with spaces). If the grid is full it is grown first. To write anywhere
other than the bottom, use update_sheet_range or insert_sheet_rows. sheet_id is the target spreadsheet's ID (the long token in its URL,
.../spreadsheets/d/<sheet_id>/edit); it is required, so confirm which
sheet to write to per call rather than assuming a default. values is
the ordered list of cell values for the new row — read the header row
first so this lines up with the existing columns. value_input_option
USER_ENTERED parses dates/numbers/formulas like typing; RAW stores text
verbatim. Returns the range that was written.
|
| update_sheet_rangeA | Write values into an explicit range, overwriting whatever is there. Use this for deterministic placement (no auto-detection): to put a new
row exactly at row n, read the sheet first, then pass
range="Sheet1!A<n>". Also used to edit an existing row in place
(e.g. change a status cell). It overwrites the target cells but does
not shift other rows — use insert_sheet_rows when you need to add
a row between existing ones. sheet_id is the spreadsheet's ID (from its URL) and is required.
range is A1 notation anchoring the top-left of the write, e.g.
Sheet1!A11 (the block grows to fit values) or an explicit
Sheet1!A11:E11. values is a list of rows, each a list of cell
strings — pass [["a", "b", "c"]] for a single row. Read the header
row first so columns line up. value_input_option USER_ENTERED parses
dates/numbers/formulas like typing; RAW stores text verbatim. Returns
the range that was written.
|
| insert_sheet_rowsA | Insert blank rows at a position, shifting existing rows down. This is the one placement that update_sheet_range can't do: it makes
room by pushing existing rows down instead of overwriting them. Use it to
prepend at the top, or to insert into a sorted region. If values is
given, the freshly inserted rows are populated in the same call. sheet_id is the spreadsheet's ID (from its URL) and is required.
tab is the worksheet title (see list_sheet_tabs). row_index
is the 1-based row number the inserted rows will occupy — content
currently at that row and below shifts down. Use 1 to insert above
everything; with a header in row 1, pass 2 to insert just below it.
values is an optional list of rows (each a list of cell strings) to
write into the new space; the number of rows inserted matches its length
(or 1 blank row when omitted). value_input_option USER_ENTERED parses
dates/numbers/formulas like typing; RAW stores text verbatim. Returns a
summary of what was inserted.
|
| list_sheet_tabsA | List the tab (worksheet) names in a spreadsheet. sheet_id is the target spreadsheet's ID (from its URL) and is
required. Use this to find the correct tab value before appending.
|
| add_sheet_tabA | Add a new tab (worksheet) to a spreadsheet. Creates an empty worksheet — the structural edit update_sheet_range
and friends can't do, since they only touch cells within tabs that
already exist. After adding, write into it with update_sheet_range
using "<title>!A1" as the range. sheet_id is the spreadsheet's ID (the long token in its URL,
.../spreadsheets/d/<sheet_id>/edit) and is required. title is the
new tab's name; it must not collide with an existing tab (see
list_sheet_tabs). index optionally sets the tab's 0-based position
in the tab strip (0 puts it first); omit to append it at the end. Returns
a summary including the new tab's numeric sheetId.
|
| rename_sheet_tabA | Rename an existing tab (worksheet). sheet_id is the spreadsheet's ID (from its URL) and is required.
tab is the current tab title (see list_sheet_tabs); new_title
is what to rename it to and must not collide with another existing tab.
Returns a summary. Note that ranges referencing the old title (e.g.
<tab>!A1) must use new_title after this.
|
| set_sheet_conditional_formatsA | Colour whole rows by rule, as conditional formatting keyed on a formula. Use this to colour-code a tracker by a status column rather than painting
cells once: the sheet recolours itself whenever the status text changes,
and nobody has to remember to repaint. Each rule in rules is
{"formula": ..., "background": ..., "foreground": ...} where
formula is a Sheets custom formula written for the first data row,
e.g. REGEXMATCH(LOWER($D2), "^applied") (no leading =; $D2
means column D of whichever row is being tested), and background /
foreground are hex colours like "#FFF3A0" (either may be omitted).
Rules are evaluated in order and the first true one paints the row, so
put the most specific (for example "closed" or "rejected") first. Every rule this tool writes carries tag inside its formula, and a
later call with the same tag deletes those rules before adding the
new set, so the call is idempotent and never touches rules a person
added by hand. Use a distinct tag per scheme if one tab carries two.
Rules apply to every column from first_row (1-based, default 2 to
skip a header) to the bottom of the grid. sheet_id is the spreadsheet's ID (from its URL) and is required.
tab is the worksheet title (see list_sheet_tabs). Returns a
summary of rules removed and added.
|
| delete_sheet_tabA | Delete a tab (worksheet) and all of its data — irreversible. This removes the entire worksheet, not just its cells; there is no undo
through the API, so confirm the right tab (and that its contents are
expendable) before calling. A spreadsheet must keep at least one tab, so
the API rejects deleting the last remaining one. sheet_id is the spreadsheet's ID (from its URL) and is required.
tab is the title of the worksheet to delete (see list_sheet_tabs).
Returns a summary.
|
| drive_update_fileA | Replace the contents of an existing Drive file, in place. This is the operation the Drive connector is missing: it writes to the
file's existing id, so the file keeps its link, its location, and its
sharing, and no duplicate is created. Drive retains the previous
content as a revision, so an overwrite is recoverable through the
file's version history in the Drive UI. file is either a Drive file id (the token in the file's URL) or a
slash-separated path from your My Drive root, e.g.
Career/consulting/rates.md. A path that matches more than one file
is an error rather than a guess. content is the complete new text —
this replaces the file, it does not append, so send the full document.
mime_type overrides the upload type; by default it is guessed from
the file's name and Drive keeps the target file's own type.
Refuses to write to Google-native files (Docs, Sheets, Slides), where a
plain-text upload would replace a formatted document with its text.
Returns the file's id, name, path, and new modified_time. |
| drive_move_fileA | Move a file or folder into a different Drive folder. Changes the item's parent; the id, name, and contents are untouched, so
existing links keep working. Moving a folder moves everything under it. file is a Drive file id or a path from My Drive root.
destination_folder is likewise an id or a path, and must resolve to
a folder. create_destination creates any missing folders along the
destination path (like mkdir -p) — it defaults to False so a typo in
the destination is an error rather than a new folder in the wrong place.
Returns the item's id and its old and new paths. |
| drive_rename_fileA | Rename a Drive file or folder in place. The id and location are unchanged, so links keep working. Drive allows
two files with the same name in one folder, so this does not stop you
creating a duplicate name — but it reports when the new name is already
in use alongside the target, since that ambiguity is what makes later
path-based tool calls fail. file is a Drive file id or a path from My Drive root. new_name
is the new name, without any path separators. Returns the old and new
name and path.
|
| drive_trash_fileA | Move a Drive file or folder to the trash (recoverable, not deleted). This is deliberately trash rather than permanent deletion: the item
stays restorable from Drive's trash for 30 days, which is the undo path
for a wrong call. There is no hard-delete tool in this server on
purpose. Trashing a folder trashes everything inside it. file is a Drive file id or a path from My Drive root. Because this
is destructive, the result echoes the resolved name, path, and id — check
those against what you intended. Returns those plus restore instructions.
|
| drive_ensure_folder_pathA | Create a folder path if it doesn't exist, and return the leaf's id. Idempotent mkdir -p for Drive: calling it twice with the same path
creates nothing the second time and returns the same id, so it is safe
for an agent to retry. Only folders are created — this never creates a
file. path is slash-separated from your My Drive root, e.g.
Career/consulting/clients/acme. Every segment that already exists is
reused; a segment that matches two existing folders is an error rather
than a guess, since continuing would build the rest of the path under an
arbitrary one of them.
Returns the leaf folder's id and path, plus created, the list of
segments that had to be made (empty when the path already existed). |
| drive_sync_folderA | Push a local folder tree into a Drive folder, creating or updating by name. Matches local files to Drive files by their path relative to the sync
root: a file that already exists is updated in place at its own id
(no duplicate), one that doesn't is created, and one whose content
already matches is left alone. Sub-folders are created as needed. This defaults to a dry run. With dry_run=True (the default)
nothing is written — you get the exact list of creates, updates, and
skips that a real run would perform. Pass dry_run=False to apply it.
Nothing is ever deleted from Drive: files present in Drive but absent
locally are left untouched. local_path is a directory on the machine running this server.
drive_folder is a Drive folder id or path from My Drive root;
create_destination makes a missing destination path (default False).
exclude is a list of glob patterns relative to the sync root — e.g.
["drafts/*", "*.tmp"]. A pattern ending in / excludes a
directory and everything under it at any depth. These are added to a
built-in list that always excludes private/, .git/, .env,
key/certificate files, and credential JSON; that built-in list cannot be
switched off by any argument. Malformed patterns abort the whole sync
rather than silently matching nothing, and every excluded file is listed
in skipped so an exclusion is never invisible.
The sync refuses to run at all — in dry run or for real — if any local
file maps onto two same-named Drive files, or onto a Google-native Doc/
Sheet/Slide that a text upload would flatten. Those appear in
collisions and native_conflicts for you to resolve by hand. |
| docs_read_textA | Read a Google Doc's text content via the Docs API. document_id is the token in the Doc's URL
(docs.google.com/document/d/<document_id>/edit). Returns
{"document_id", "title", "text"} — the plain text of the body,
paragraphs joined with newlines, formatting not represented. Read
before docs_replace_body so the rewrite starts from what is
actually there.
|
| docs_append_textA | Append plain text to the end of a Google Doc. Starts a new paragraph at the end of the body (the text is inserted
after the current last paragraph). The Doc keeps its formatting; the
appended text arrives unstyled. Use this for logs, addenda, and
updates that should not disturb what is already written. |
| docs_replace_bodyA | Replace a Google Doc's entire body with plain text. The Doc keeps its id, link, title, sharing, and revision history (the
old body is recoverable from version history in the Docs UI), but
all formatting is flattened — this is for Docs that work as living
text documents, not designed artifacts. Read with docs_read_text
first and send the complete new body; this replaces, it does not merge. |
| ats_fetch_boardA | Read one company's job board from its ATS's public API, normalized. Use this instead of fetching a careers page: Greenhouse, Lever, and Ashby
boards are JavaScript shells whose HTML contains no postings, so a page
fetch returns an empty-looking board for a company that is actively
hiring. This reads the same postings the board renders, as JSON. platform is greenhouse, lever, ashby, or workday —
the four that publish a board API. iCIMS, SuccessFactors, and Taleo
publish none, so coverage of employers on those systems is deliberately
partial; cross-check them against an aggregator. slug is the bare
board slug, not a URL — the last path segment of the board address
(labelbox, people-ai, handshake) — except for workday,
whose identity has three parts: pass tenant.wdN/site
(adobe.wd5/external_experienced) or the board URL itself. Workday
reads are paged 20 at a time, so large tenants are slow when unfiltered
— pass a title_filter and it is pushed down as a Workday search,
which is cheap; Workday postings carry approximate day-resolution
posted dates and never compensation. Pass company with the
company you expect this slug to belong to and the result is checked
against the board's own name: mismatches come back as NAME_MISMATCH
with the jobs still attached, which is how you catch a slug that points
at a different company of the same name. Only Greenhouse publishes a
board name, so this check is inert for Lever and Ashby.
title_filter keeps postings whose title contains any of the given
strings (case-insensitive). updated_since is an ISO date that keeps
postings posted or updated at/after it. keywords is different from
both: it never drops a posting — every returned job gains a
keyword_hits list naming which of your keywords its title, team, or
description mentions. Use it for ranking signal in high-volume searches
(e.g. a stack list like ["Salesforce", "HubSpot", "Clari"]); since
stacks are named in descriptions, pair it with include_descriptions
for meaningful hits. include_descriptions adds a
plain-text snippet per job (~1,500 chars) and makes the Greenhouse
request much larger, so it defaults off. full_descriptions goes one
step further and returns each posting's whole description, untruncated
(it implies include_descriptions): use it to go deep on one board
whose roles a sweep has already surfaced, paired with a title_filter
or a small limit so the read stays readable — long postings run to
several thousand characters each. Keyword hits are then found anywhere
in the posting, not only in its first 1,500 characters. The field is
still named description_snippet so pipelines see one shape.
limit/offset page through the matches, response_format is
markdown (a table, for reading) or json (for pipelines).
The result always reports total_on_board, matched (after your
filters), and count (this page), so a filtered or paginated read can
never be mistaken for the whole board. Board trouble is reported as a
status on the result — BOARD_EMPTY, SLUG_NOT_FOUND,
NOT_FOUND_OR_API_DISABLED, RATE_LIMITED, UPSTREAM_ERROR,
TIMEOUT — each with a detail saying what it means and what to do
next. An empty board is not a dead company, and a 404 is not proof of
one; read the detail before concluding anything. Compensation is
reported only where the platform publishes it as structured data, never
read out of the description text. |
| ats_sweep_boardsA | Sweep many job boards in one call — the watchlist sweep, batched. boards is a list of up to 50 objects, each {"company": "Arize AI", "platform": "greenhouse", "slug": "arizeai"}. company is both
echoed back on that board's result and compared against the board's own
reported name, so a slug that has drifted to a different company shows up
as NAME_MISMATCH rather than as plausible-looking jobs. The other
arguments work exactly as in ats_fetch_board and apply to every
board; limit_per_board caps postings returned per board (default 25).
include_descriptions defaults off here because it multiplies the
size of every Greenhouse request in the sweep — turn it on when you are
passing keywords and the hits need to see description text. There is
deliberately no full_descriptions here: a sweep is for breadth, and
untruncated postings across many boards would not fit in a context
window. Go deep on one board with ats_fetch_board.
One board failing never fails the sweep. Every board comes back with
its own status and detail; the ones that failed carry an empty
jobs list and an explanation, and results stay in the order you
passed them so they line up with your roster. The summary counts
boards read versus failed and total matching postings — check it against
the number of boards you sent before drawing conclusions from the
results, and never fill in a board that reported a failure. Boards are fetched a few at a time with polite pacing, so a large sweep
takes tens of seconds rather than being instant. Seen-state. Every sweep returns a state snapshot — boards keyed
platform:slug, each with its postings keyed by stable job id (with
first_seen dates) and a running history of board totals. Save it
(e.g. as a JSON file next to your other career records) and pass it back
as previous_state on the next sweep to get changes: new_jobs
and gone_jobs since that run, board_status_changes,
total_deltas (a per-company hiring-velocity signal), and
new_boards for roster additions (whose postings are deliberately not
counted as new jobs). A board that fails to read carries its previous
jobs forward rather than reporting them all gone, and boards in
previous_state that this sweep didn't include ride along verbatim —
so a roster split into batches (e.g. to fit a time limit) can write
each batch's returned state straight back without erasing the others;
retire a board by deleting its key from the saved state file, not just
by dropping it from the roster. State is relative to
the filters used — keep filters stable between runs, or expect
filter_changed: true warning that the diff reflects the filter, not
the market. Use response_format="json" when round-tripping state;
the markdown rendering shows the changes but not the snapshot. |
| ats_check_boardA | Check whether a board is still alive, without pulling any job data. The cheap quarterly pass over a roster: it answers "is this slug still a
real board, is it still this company, and is anything posted on it" and
returns counts only — no postings. Use ats_fetch_board when you want
the jobs. Returns status, board_name (the org name the API reports, where
the platform publishes one), total_on_board, checked_at, and
detail. Pass company to have the board's name checked against
what you expected; a NAME_MISMATCH means the slug and the company
have come apart, which is what an acquisition or a rename looks like from
here. As everywhere in these tools, a SLUG_NOT_FOUND or
NOT_FOUND_OR_API_DISABLED is a prompt to go check, not a finding that
the company is gone. |
| ats_ashby_formA | Read one Ashby posting's application form — every question, verbatim. Use this before building an application packet: it reads the same
applicant-side GraphQL the job board renders from and reports each
section and field with its title, type, required flag, stable path
identifier, and the exact choices of any select. The summary line
classifies the form — "clicks-and-resume only" (no required free-text)
versus how many required free-text questions need workshopped answers —
which is the fact that decides whether an application needs drafting
time or just approval. This reads the form; it never writes to it, and
nothing here submits anything. slug is the board slug (jobs.ashbyhq.com/{slug}), and
job_posting_id is the posting UUID from the posting URL — both are
in ats_fetch_board results. Trouble is in-band, as status +
detail (NOT_FOUND, UPSTREAM_ERROR, RATE_LIMITED,
TIMEOUT); a NOT_FOUND usually means the posting closed, which is an
answer, not an error. Post-submit survey forms (usually demographics)
are counted but not expanded.
|
| check_postingA | Check whether a job posting's canonical page is still live. Fetches url from this server (not from the caller's network, so it
works from sandboxes that block employer domains) and reports one of
three statuses. LIVE: the page loaded with a closed notice absent
and an application path present. DEAD: HTTP 404/410, another 4xx,
or a "no longer accepting applications" style notice on the page.
UNVERIFIABLE: a JavaScript shell with nothing to read, a 5xx or a
bot block, a timeout, a refused URL, or a page that loaded without a
recognisable apply control — this is not a finding about the posting,
and the caller's rule is to hold the lead rather than drop or surface
it. Pass expected_title (a distinctive fragment of the job title)
to guard against a careers site that redirects dead postings to its
listing page with a 200: if the title is missing the result is
UNVERIFIABLE, not LIVE. For Greenhouse, Lever, Ashby, and Workday postings prefer
ats_fetch_board with a title_filter: the board API is
authoritative and needs no page read. Use this for employer-hosted
careers sites and aggregator-sourced leads. Only public https URLs are fetched; the request carries no
credentials and follows at most five redirects, each checked. |
| http_requestA | Make an HTTP request and return the response body (truncated). Use for quick API calls and connector prototyping. json_body is sent
as a JSON payload for POST/PUT/PATCH. |