Skip to main content
Glama
Matthew3957

ai-toolkit

by Matthew3957
README.md
# ai-toolkit

A personal [MCP](https://modelcontextprotocol.io) server for your own tools,
connectors, and prompts. Runs locally over stdio.

## Layout

```
src/ai_toolkit/
  server.py            # builds FastMCP, registers every capability (_REGISTRARS)
  config.py            # env-based config + local data dir
  auth.py              # Google OAuth proxy for the HTTP transport
  google_client.py     # shared: whose Google identity a call acts as, + retry/backoff
  google_auth.py       # cached-token loading for the local stdio path
  health.py            # unauthenticated /health, incl. token-staleness warnings
  ats_boards.py        # pure ATS logic: URLs, normalizers, filters, rendering
  drive_sync.py        # pure sync logic: paths, fail-closed exclusions, plans
  tools/
    ats.py             # Greenhouse/Lever/Ashby job-board reads (public APIs)
    drive.py           # update/move/rename/trash/mkdir -p/sync for Google Drive
    sheets.py          # Google Sheets read/write
    notes.py           # save/list/search local markdown notes
    web.py             # generic HTTP connector (stdio only by default)
  prompts/
    library.py         # reusable prompt templates (code_review, summarize)
```

Every module exposes `register(mcp)`. To add a capability, write the module and
add one line to `_REGISTRARS` in `server.py`. Capabilities you want to deploy but
not publish can live in an optional `src/ai_toolkit/extensions.py` that exports a
`REGISTRARS` tuple; `server.py` picks it up when it exists.

## Install & run

```bash
uv sync                       # create venv + install deps
uv run ai-toolkit             # run the server over stdio (for testing)
uv run pytest                 # run the test suite (no credentials needed)
```

## Google auth

One-time consent, then a check you can run before deploying:

```bash
uv run python -m ai_toolkit.scripts.authorize_google   # opens a browser once
uv run ai-toolkit-check-auth                           # verifies it actually works
```

`check-auth` confirms a token exists, carries every scope the tools need, and is
accepted by Drive and Sheets on a real read-only call. It exits non-zero on
failure, so it can gate a deploy. It only covers the local path — on the fly.io
deployment each caller signs in as themselves, so check `/health` there instead.

**Adding a scope invalidates nothing automatically.** A token minted before a
scope existed keeps working for the older APIs and fails only on the new ones,
which looks like a broken tool rather than stale auth. After changing
`config.GOOGLE_SCOPES` or `auth.REQUIRED_SCOPES`, re-run `authorize_google`
locally and re-consent in the connector remotely.

## Drive tools

The Drive connector in a Claude session can create files and folders but has no
update-in-place, move, rename, or delete, so every revision leaves a duplicate.
These fill that gap:

| Tool | What it does |
| --- | --- |
| `drive_update_file` | Replace a file's contents **at its own id** — no duplicate |
| `drive_move_file` | Reparent a file or folder |
| `drive_rename_file` | Rename in place |
| `drive_trash_file` | Move to trash (recoverable for 30 days; there is no hard delete) |
| `drive_ensure_folder_path` | Idempotent `mkdir -p` by path, returns the leaf id |
| `drive_sync_folder` | Push a local tree into Drive, create-or-update by name |

Every tool takes either a Drive file id or a slash-separated path from your My
Drive root (`Career/consulting/rates.md`), and returns both, so a result can be
pasted straight back in as an argument. A path matching more than one Drive file
is an error, never a guess.

`drive_sync_folder` **defaults to a dry run** — call it once to see the plan,
then pass `dry_run=false` to apply exactly that plan. It never deletes from
Drive. Its `exclude` patterns are *added to* a built-in denylist (`private/`,
`.git/`, `.env`, key/cert files, credential JSON) that no argument can switch
off; see `ALWAYS_EXCLUDE` in `drive_sync.py`. Malformed patterns abort the whole
sync rather than silently matching nothing, and every skipped file is reported.

## ATS job-board tools

A modern job board is a JavaScript shell: fetching the careers page returns
markup with no postings in it. An agent that can't tell "no postings" from
"nothing rendered" reports a hiring company as dead. Greenhouse, Lever, Ashby,
and Workday all serve the same postings as JSON without auth, so these tools
read that instead. No credentials, no writes, and no requests anywhere but
those hosts (for Workday: validated `*.myworkdayjobs.com` subdomains).

| Tool | What it does |
| --- | --- |
| `ats_fetch_board` | One board's postings, normalized across platforms, filtered and paginated |
| `ats_sweep_boards` | Up to 50 boards in one call, failures isolated per board |
| `ats_check_board` | Is this board alive and still this company? Counts only, no job data |

Every result carries **three separate counts** — `total_on_board` (before
filtering), `matched` (after your filters), `count` (this page) — plus
`has_more`/`next_offset`. That is the whole point: a filtered or paginated read
can never be mistaken for the full board.

Every result also carries a **board identity**. Pass the company you expect and
the board's own reported name is checked against it, because a slug pointing at
the wrong company is the failure that quietly poisons a whole run — Greenhouse
`galileo` is Galileo Health's board, not Galileo AI's. Only Greenhouse publishes
an org name, so this check is inert on Lever and Ashby; verify those slugs by eye
when adding them.

Compensation is read **only** from structured API fields, never parsed out of
description text.

### Status taxonomy

Statuses come back *inside* the result, never as a tool error, and each carries a
`detail` saying what to do next. The two that matter most are the ones that look
like death and aren't:

| Status | Meaning |
| --- | --- |
| `OK` | Board read, at least one posting |
| `BOARD_EMPTY` | Live board, zero postings — the company is quiet, not dead. Don't drop it from the roster |
| `SLUG_NOT_FOUND` | 404 on Greenhouse/Lever — wrong slug, or an acquisition/rename. Web-search the company's careers page before concluding anything |
| `NOT_FOUND_OR_API_DISABLED` | 404 on **Ashby**, which is ambiguous: an org can switch this API off while its board site stays live (Vellum does). Check `jobs.ashbyhq.com/<slug>` before treating the company as gone |
| `NAME_MISMATCH` | Warning, jobs still returned — the board's name isn't the company you expected |
| `RATE_LIMITED` | 429 after retries — back off, retry next run |
| `UPSTREAM_ERROR` / `TIMEOUT` | 5xx or no response after retries — transient, do not mark the company dead |

In a sweep, `boards_failed` counts only boards that could not be read;
`BOARD_EMPTY` and `NAME_MISMATCH` count as read.

### Examples

Fetch one board, filtered, as JSON:

```json
{"tool": "ats_fetch_board", "arguments": {
  "platform": "greenhouse", "slug": "anthropic", "company": "Anthropic",
  "title_filter": ["education", "enablement"], "limit": 25,
  "response_format": "json"
}}
```

Go deep on one board once a sweep has surfaced it — whole postings, untruncated,
so a fit read or a stack keyword sees the full text rather than the first ~1,500
characters. Pair it with a `title_filter` or a small `limit`; long postings run to
several thousand characters each. The sweep has no such switch on purpose:

```json
{"tool": "ats_fetch_board", "arguments": {
  "platform": "ashby", "slug": "mistral", "company": "Mistral AI",
  "title_filter": ["enablement", "developer educator"],
  "full_descriptions": true, "response_format": "markdown"
}}
```

Sweep a roster — one call, failures isolated, results in the order you sent them:

```json
{"tool": "ats_sweep_boards", "arguments": {
  "boards": [
    {"company": "Arize AI", "platform": "greenhouse", "slug": "arizeai"},
    {"company": "People.ai", "platform": "lever", "slug": "people-ai"},
    {"company": "Handshake", "platform": "ashby", "slug": "handshake"}
  ],
  "updated_since": "2026-08-01", "limit_per_board": 10
}}
```

Quarterly health check on a slug you suspect has drifted:

```json
{"tool": "ats_check_board", "arguments": {
  "platform": "greenhouse", "slug": "galileo", "company": "Galileo AI"
}}
```

`slug` is always the **bare slug**, never a URL: for
`https://jobs.lever.co/people-ai` it is `people-ai`. Passing a URL is rejected
with that guidance rather than silently failing. The exception is Workday,
whose board identity has three parts: pass `tenant.wdN/site`
(`adobe.wd5/external_experienced`) or paste the board URL itself. Workday reads
are paged 20 postings at a time, so pass a `title_filter` on large tenants —
it is pushed down as a Workday search and costs a couple of requests instead
of dozens of pages. Workday posted dates are approximate (day resolution, from
"Posted N Days Ago"); "30+ Days Ago" and non-English strings stay null.

Sweeps also return a **seen-state snapshot**: save the payload's `state` (a
JSON file in the caller's own records) and pass it back as `previous_state` on
the next sweep to get mechanical diffs — `new_jobs`, `gone_jobs`, board status
changes, and per-board total deltas (a free hiring-velocity signal). A board
that fails to read carries its last-good jobs forward, so a timeout never
looks like every posting disappearing.

Out of scope by design: iCIMS, SuccessFactors, and Taleo (no public API —
coverage of employers on those systems is **permanently partial**; cross-check
them with an aggregator), anything that applies or writes, HTML scraping of
career pages, and comp read out of description text.

### Testing these

`uv run pytest` covers them offline: the normalizers run against real responses
captured in `tests/fixtures/`, and the tools run against an `httpx.MockTransport`
so the retry, pacing, and status-code paths are exercised without the network.
The live checks against the real boards are opt-in:

```bash
ATS_LIVE_TESTS=1 uv run pytest tests/test_ats_live.py
```

They assert on shape and status handling, not on job counts, which drift weekly.
`evals/ats_sweep_eval.json` covers agent usability rather than code correctness.

## Register with Claude Code

```bash
claude mcp add ai-toolkit -- uv run --directory /path/to/ai-toolkit ai-toolkit
```

Or add to your MCP client config (e.g. Claude Desktop `claude_desktop_config.json`):

```json
{
  "mcpServers": {
    "ai-toolkit": {
      "command": "uv",
      "args": ["run", "--directory", "/path/to/ai-toolkit", "ai-toolkit"],
      "env": {}
    }
  }
}
```

## Adding a named connector

Copy `tools/web.py` to `tools/<service>.py`, read the API key via
`config.get_secret("MY_TOKEN")`, hardcode the base URL + auth header, and expose
purpose-built `@mcp.tool()` functions. Register it in `server.py`.

## Roadmap

- [ ] Resources (expose readable data/context)
- [ ] Automated runs (scheduled background jobs — see notes below)

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