Skip to main content
Glama
Matthew3957

ai-toolkit

by Matthew3957

Server Configuration

Describes the environment variables required to run the server.

NameRequiredDescriptionDefault

No arguments

Instructions

Guidance the server publishes about itself, which clients place ahead of the tool catalog so the model reads it before choosing anything.

This server publishes no instructions, or was last inspected before Glama recorded them.

Capabilities

Features and capabilities supported by this server

Protocol revision2025-11-25

CapabilityDetails
tools
{
  "listChanged": true
}
logging
{}
prompts
{
  "listChanged": false
}
resources
{
  "subscribe": false,
  "listChanged": false
}
extensions
{
  "io.modelcontextprotocol/ui": {}
}
experimental
{}

Tools

Functions exposed to the LLM to take actions

NameDescription
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.

Prompts

Interactive templates invoked by user choice

NameDescription
code_reviewReview a diff with a focus area.
summarizeSummarize text in a chosen style.

Resources

Contextual data attached and managed by the client

NameDescription

No resources

TDQS

A4/5.0

Scored across 27 tools

Disambiguation4/5

Tools are grouped by domain (Sheets, Drive, Docs, ATS, notes) and within each group target distinct operations: read vs. append vs. update vs. insert for sheets, and fetch vs. sweep vs. check for ATS. The only mild overlap is between ats_check_board and check_posting, but descriptions clearly separate board-level from URL-level checks. Overall an agent can reliably select the right tool.

Naming Consistency4/5

Most tools follow a verb_noun pattern, with domain prefixes for larger clusters (drive_*, docs_*, ats_*) and bare verb_noun names for notes and HTTP (save_note, list_notes, search_notes). The pattern is predictable and consistent within each domain. The main exception is http_request, which is a noun phrase rather than verb_noun, but it's a generic utility rather than a domain operation.

Tool Count4/5

At 27 tools, the surface is large, but it is justified by covering five distinct domains (Sheets, Drive, Docs, ATS, notes) plus a generic HTTP client. Each tool addresses a specific need within its domain, so none feels redundant. It is slightly dense but not bloated relative to the server's stated 'toolkit' purpose.

Completeness4/5

Sheets, Drive, and Docs each have solid CRUD-like coverage: read/append/update/insert for sheets, read/append/replace for Docs, and update/move/rename/trash/sync for Drive. The ATS tools are notably thorough, covering fetch, sweep, check, and form inspection. Minor gaps include no create-doc/create-sheet tool (though sync can upload files) and no board submission, but these are reasonable omissions for a read/update-oriented toolkit.

Maintenance

ActivityMaintained
ResponsivenessNo issues