Skip to main content
Glama

claude-tools

Local MCP servers that give Claude Code access to other tools mid-session.

gemini-bridge

Exposes three tools backed by the Gemini CLI:

  • ask_gemini(prompt, context="") — ask Gemini a question, e.g. for a second opinion on an approach.

  • review_diff(repo_path, instructions="") — runs git diff in repo_path and sends it to Gemini for critique. Pass the absolute path of the repo you want reviewed; the bridge runs as its own background process and does not share Claude Code's working directory.

  • ask_gemini_about_files(file_paths, question) — reads one or more full files and asks Gemini a question about them. Use this instead of ask_gemini's context param when the files are too large for Claude's own context, or when you want Gemini's take on whole files/modules rather than a truncated excerpt — Gemini's window is large enough to hold much more (up to 500k chars) than ask_gemini/review_diff allow (60k chars). Pass absolute paths; the bridge runs as its own process and does not share Claude Code's cwd.

Every call is logged to log.jsonl (gitignored) as an audit trail of what was asked and answered.

Setup

  1. Install the Gemini CLI:

    npm install -g @google/gemini-cli
  2. Authenticate with an API key — as of gemini-cli 0.50.0, the free oauth-personal login tier ("Gemini Code Assist for individuals") is no longer accepted; Google points individual users at a separate product (Antigravity) instead. Use an API key:

    • Get a free key from Google AI Studio.

    • Put it in ~/.gemini/.env:

      GEMINI_API_KEY=your-key-here
    • Set ~/.gemini/settings.json to use it:

      {
        "security": { "auth": { "selectedType": "gemini-api-key" } }
      }
  3. Install this project's dependencies:

    cd ~/git/claude-tools
    python3 -m venv .venv
    .venv/bin/pip install -r requirements.txt
  4. Register the server with Claude Code (user scope, so it's available in every project):

    claude mcp add gemini-bridge --scope user -- \
      ~/git/claude-tools/.venv/bin/python ~/git/claude-tools/gemini_bridge.py
  5. Restart Claude Code / reload the window. ask_gemini, review_diff, and ask_gemini_about_files should show up as callable tools.

Notes

  • gemini_bridge.py reads ~/.gemini/.env itself and sets GEMINI_CLI_TRUST_WORKSPACE=true on every call, rather than relying on gemini-cli's own env-file auto-discovery (unreliable when invoked as a subprocess from an arbitrary cwd) or its interactive trusted-folder prompt (which headless calls can't answer).

  • Gemini CLI's -p/--prompt non-interactive flag is flagged upstream as a candidate for future deprecation (gemini-cli#16025). If it's renamed, only _call_gemini() in gemini_bridge.py needs updating.

  • Context and diffs are truncated to 60k characters before being sent to Gemini to avoid blowing past its context window. ask_gemini_about_files uses a much higher 500k-character ceiling, since its whole point is to use Gemini's larger window on full files.

  • Before invoking the Gemini CLI, _call_gemini() does a quick 5s TCP preflight check against Gemini's API host. On a flaky connection (e.g. a phone hotspot), this fails fast with a clear error instead of blocking for the full GEMINI_TIMEOUT (120s). Restart Claude Code / reload the window after pulling this change, since the running server process won't pick it up otherwise.

Related MCP server: mcp-local-llm

ollama-bridge

Exposes three tools backed by a locally running Ollama instance:

  • prefilter_diff(repo_path, model="qwen2.5-coder:7b") — runs git diff in repo_path and sends it to a local Ollama model for a cheap first-pass triage before spending a review_diff (Gemini) call on it. The response starts with CLEAN: or FLAGGED: — only escalate to review_diff when it's FLAGGED, or when this tool errors (e.g. Ollama isn't running); never skip review outright just because the local pass errored.

  • triage_log(repo_path, log_path, model="qwen2.5-coder:7b") — reads a build/test log already written to disk (redirect a failing command's output first, e.g. cmd > out.log 2>&1) and sends it to a local Ollama model to pull out just the root failure (FILE:/ERROR:/CONTEXT:), instead of reading the whole raw log directly. Returns NO FAILURE FOUND. for a clean run. log_path must resolve inside repo_path (absolute or relative, same containment rule as repo-bridge's get_file) — rejected otherwise.

  • triage_transcript(repo_path, transcript_path, model="qwen2.5-coder:7b") — reads a transcript JSON file already written by youtube-bridge's transcribe_video (a list of {start, end, text} segments) and sends it to a local Ollama model to flag likely restarts, filler-heavy stretches, and dead air, instead of reading the whole transcript closely to spot them. Returns [MM:SS-MM:SS] timestamps with a one-line reason each, or CLEAN: ... if nothing stands out. Same repo_path/transcript_path containment rule as triage_log. Doesn't decide cuts — points at where to look before building keep_segments for cut_video.

Every call is logged to ollama_log.jsonl (gitignored) as an audit trail.

Setup

  1. Install Ollama and pull a coding-capable model:

    ollama pull qwen2.5-coder:7b
  2. This tool has no new dependency beyond what's already in requirements.txt — it talks to Ollama's local REST API with the standard library's urllib, consistent with the rest of this repo's bridges.

  3. Register the server with Claude Code (user scope):

    claude mcp add ollama-bridge --scope user -- \
      ~/git/claude-tools/.venv/bin/python ~/git/claude-tools/ollama_bridge.py
  4. Restart Claude Code / reload the window. prefilter_diff should show up as a callable tool.

Notes

  • This exists to save Gemini API calls on routine commits, not to replace review_diff. A quantized 7B local model catches the obvious stuff (dead code, naming, syntax slips) but reliably misses subtler bugs and security issues a frontier model catches — treat a CLEAN verdict as "nothing obvious jumped out," not "safe to skip a real review," for anything consequential.

  • Reuses review_diff's own diff-selection order (vs HEAD, then staged --cached, then plain unstaged) so both tools always look at the same diff.

  • _call_ollama sets an explicit num_ctx: 8192 on every request. Ollama defaults to 2048 regardless of the model's real context length, which would silently left-truncate any diff near MAX_CONTEXT_CHARS and drop PREFILTER_INSTRUCTIONS entirely — caught by review_diff on this tool's own first commit before it ever shipped. temperature: 0.0 is set alongside it for a more consistent CLEAN/FLAGGED verdict.

  • Verified end-to-end against this repo's own pending README.md diff on 2026-07-24: prefilter_diff picked up the real diff and returned a CLEAN verdict from qwen2.5-coder:7b running locally — call and response both landed in ollama_log.jsonl as expected.

  • qwen2.5-coder:7b was picked as the default because it's the only locally installed model (of qwen2.5-coder, dolphin-mistral, llama3, deepseek-coder, plus several custom personas) that reports tool-use/code capability rather than plain chat completion — override via model for a different local model.

  • triage_log reads an existing log file rather than executing a command itself — deliberately, to keep this bridge's subprocess surface limited to git diff (a fixed, safe command) rather than adding an arbitrary-command-execution tool. Redirect a failing command's output to a file first (cmd > out.log 2>&1), then triage that.

  • triage_log truncates from the start of the log via _truncate_keep_tail, the opposite of prefilter_diff's _truncate — a build/test failure is almost always near the end of a log (the start is setup noise: dependency resolution, banners), unlike a diff where head-truncation is fine. Uses a larger num_ctx: 24576 (LOG_NUM_CTX) than prefilter_diff's 8192 since MAX_LOG_CHARS (40k chars) needs more headroom than a same-ratio scale-up would give — review_diff flagged that 40k chars could run denser than ~2 chars/token on symbol-heavy logs, so LOG_NUM_CTX was set with margin rather than scaled proportionally to prefilter_diff's ratio.

  • triage_log requires repo_path and resolves log_path inside it via the same _resolve_in_repo containment check repo-bridge uses for get_file — added after review_diff flagged the first draft (a bare log_path with no scoping) as an arbitrary-file-read risk: an unscoped path could be pointed at ~/.ssh, .env files, etc.

  • _call_ollama's URLError handler checks isinstance(e.reason, TimeoutError) before the generic OSError check — review_diff caught that a real (slow-but-reachable-Ollama) timeout is itself an OSError subclass, so without checking TimeoutError first it was misreported as "not reachable - is ollama serve running?" instead of a timeout.

  • triage_log always appends a pointer back to the full log_path and line/char count alongside the model's summary — a 7B model can misidentify the root cause in a complex multi-error log, so the raw log stays one Read away rather than being fully replaced by the summary.

  • Verified against a synthetic pytest log (42 tests, 1 real failure buried in passing-test noise): correctly extracted the failing file/line, exact AssertionError, and relevant traceback lines, ignoring the noise. A second synthetic clean-run log correctly returned NO FAILURE FOUND.

  • triage_transcript (added 2026-08-06) reuses _resolve_in_repo from triage_log and _truncate from prefilter_diff rather than adding new helpers — a transcript is closer to a diff than a log for truncation purposes (an issue can be anywhere in it, not clustered near the end), so head-truncation is the right default, not triage_log's tail-keeping one. Verified end-to-end against a synthetic 7-segment transcript containing one obvious mid-sentence restart and one filler-heavy stretch ("um, so, yeah, so basically, um") — qwen2.5-coder:7b flagged both correctly with their timestamps. Does not decide keep_segments itself, by design — cut_video's docstring is explicit that cut decisions belong to the agent reading the actual transcript, not a local model summary.

tn3270-bridge

Exposes six tools backed by py3270 (which drives s3270 under the hood) for automating TN3270 mainframe green-screen sessions:

  • connect(host, port=23) — opens a session, returns a session_id.

  • send_keys(session_id, keys) — types keys into the current field. A "\n" in keys presses Enter (e.g. "myuser\n" types myuser and submits it). Returns the resulting screen.

  • read_screen(session_id, structured=False) — returns the current screen as plain text. With structured=True, returns JSON instead: {"cursor": {"row", "col"}, "fields": [{"row", "col", "protected", "hidden", "autoskip", "modified", "text"}, ...]} — use this when an agent needs to know where to type (which field is unprotected, which one is a hidden password field) rather than just what's on screen. Row/col are 1-indexed and refer to the field's attribute-byte position, so the first typeable character is one column to the right of col.

  • read_panel_state(session_id) — returns a compact JSON summary instead of a full screen dump: {"messages": [{"id", "severity", "text"}, ...], "actionable_inputs": [{"label", "row", "col", "hidden"}, ...], "cursor": {"row", "col"}}. messages is every line starting with a standard IBM message ID (ICH408I, IKJ56650I, etc.) — RACF/TSO/JES errors and status lines all follow that shape, so this generalizes across products instead of needing a per-panel lookup table. actionable_inputs is every unprotected field with a best-guess label pulled from the protected field immediately before it (e.g. "Userid ===>" next to the input). Use this instead of read_screen when the agent just needs to know what the screen says and where to type, not the full layout.

  • send_function_key(session_id, key) — sends PFn/PAn/Clear/Enter. Returns the resulting screen.

  • disconnect(session_id) — closes the session.

Sessions live in memory for the lifetime of the server process (one per Claude Code session) — disconnect any session you're done with rather than letting it leak. Every call is logged to tn3270_log.jsonl (gitignored).

Setup

  1. Install s3270 (part of the x3270 suite):

    brew install x3270
  2. Install this project's dependencies (shared .venv with gemini-bridge):

    cd ~/git/claude-tools
    .venv/bin/pip install -r requirements.txt
  3. Register the server with Claude Code:

    claude mcp add tn3270-bridge --scope user -- \
      ~/git/claude-tools/.venv/bin/python ~/git/claude-tools/tn3270_bridge.py
  4. Restart Claude Code / reload the window.

Notes

  • Plain-text read_screen uses x3270's Ascii() script command. Structured mode uses ReadBuffer(Ascii), which annotates the buffer with SF(...) markers at each field's attribute byte; the byte's bits are decoded per the 3270 field-attribute spec (IBM GA23-0059) into protected/hidden/ autoskip/modified. hidden is what marks password fields — verified against a live TSO logon screen, where Password/New Password/MFA Token all came back hidden: true and Userid did not.

  • Field row/col and cursor row/col come from two different x3270 commands with different indexing (ReadBuffer is 1-indexed, Query (Cursor) is 0-indexed) — _structured_screen() normalizes both to 1-indexed. Confirmed by checking the cursor lands one column past an unprotected field's attribute byte, which is where typing actually starts.

  • The first five tools have been exercised against a live TN3270 host (a local test mainframe on localhost:3270): connect + read_screen pulled back the logon banner, send_keys("TSO\n") advanced from the logon-type prompt to the TSO/E LOGON screen, send_function_key("PF3") logged off back to the banner, and structured read_screen correctly identified the one unprotected field on the banner screen and the hidden password fields on the TSO/E LOGON screen.

  • read_panel_state (added 2026-08-06) has not been verified against a live host in this environment — no test mainframe was reachable, only unit tested against synthetic screen data (a fabricated RACF-style logon denial: ICH70001I/ICH408I message lines plus Userid/Password fields), which it parsed correctly. The message-ID regex is based on IBM's documented, standardized message format, and the label heuristic matches every real panel layout seen in this repo's earlier live testing, but both should be treated as unverified against a real host until run against one. actionable_inputs labels are a best guess, not authoritative — fall back to structured read_screen if a label looks wrong.

repo-bridge

Exposes four tools for getting codebase context from a repo outside Claude Code's own working directory:

  • search_codebase(repo_path, query, max_results=50, ignore_case=False) — greps repo_path for query (a regex). Uses git grep when repo_path is a git repo (respects .gitignore), otherwise plain grep -r. Returns path:line:content per match.

  • get_file(repo_path, path) — returns the full contents of path (relative to repo_path), truncated past 60k chars. Rejects paths that escape repo_path (e.g. ../../etc/passwd).

  • list_structure(repo_path, max_entries=500) — returns a directory tree as indented text. Uses git ls-files (tracked files only) when repo_path is a git repo, otherwise walks the filesystem skipping common junk dirs (node_modules, .venv, __pycache__, etc.).

  • get_symbol(repo_path, name, language="", max_results=10) — finds function/class/method/type definitions named name and returns their source text via tree-sitter. Supports python, javascript, typescript, tsx, go, rust, java, ruby, c, and cpp. Greps for files that reference name first rather than parsing the whole repo, and returns every match found (up to max_results), not just the first.

Pass the absolute path of the repo you want as repo_path for every tool — this server runs as its own process and does not share Claude Code's working directory.

Setup

  1. Install this project's dependencies (shared .venv):

    cd ~/git/claude-tools
    .venv/bin/pip install -r requirements.txt
  2. Register the server with Claude Code:

    claude mcp add repo-bridge --scope user -- \
      ~/git/claude-tools/.venv/bin/python ~/git/claude-tools/repo_bridge.py
  3. Restart Claude Code / reload the window.

Notes

  • Uses the official per-language tree-sitter-* packages (prebuilt wheels, compiled at install time), not the tree-sitter-language-pack package — that one downloads grammars over the network on first use, which is a bad fit for this repo's other lesson-learned (see gemini-bridge's network preflight check above) and just failed outright when tried on a flaky connection.

  • get_symbol matches by each grammar's name field on definition-like node types (function_definition, class_declaration, etc.) — this works generically across languages without per-language field lookups. _find_definitions() walks the whole tree (including inside already-matched nodes, so e.g. two same-named methods in two different classes in one file both surface) and results are capped at max_results, defaulting to 10.

  • C and C++ function_definition nodes are the one exception to the generic name-field lookup above: their identifier is nested inside a declarator chain (a pointer_declarator for pointer return types, etc.) ending in a function_declarator whose own declarator field is the actual identifier. _c_family_function_name() in repo_bridge.py unwraps that chain; struct/enum/union/class specifiers in C/C++ do expose a name field directly and don't need it.

  • Verified against this repo (Python) and small standalone fixtures for TypeScript, Go, Rust, Java, Ruby, C, and C++ — get_symbol correctly pulled a function/struct (or class/method) from each, with correct line ranges.

linkedin-bridge

Exposes three tools for managing LinkedIn posts on behalf of the authenticated member, all via LinkedIn's Posts API:

  • post_to_linkedin(text, visibility="PUBLIC") — publishes a text post. visibility is "PUBLIC" or "CONNECTIONS". Returns the live post URL on success.

  • update_linkedin_post(post_url_or_urn, text) — replaces the text of an existing post. Takes either the live URL or a bare URN. Only needs the w_member_social scope this app already has.

  • delete_linkedin_post(post_url_or_urn) — permanently deletes a post. Irreversible.

All three publish, edit, or delete under the user's real identity — always confirm the exact action/text before calling any of them, never call them unprompted.

Every call is logged to linkedin_log.jsonl (gitignored) as an audit trail.

Setup

  1. Create a LinkedIn Company Page, then a developer app at linkedin.com/developers/apps associated with it, with the Share on LinkedIn product added (grants the w_member_social scope). The app needs a real privacy policy URL — this repo's own consulting site's privacy page is an example of a minimal one.

  2. Add an Authorized redirect URL of http://localhost:8765/callback on the app's Auth tab, and note the Client ID and Client Secret.

  3. Put the credentials in ~/.linkedin/.env (create the file yourself in a text editor — don't paste secrets through an agent if avoidable):

    LINKEDIN_CLIENT_ID=...
    LINKEDIN_CLIENT_SECRET=...

    then chmod 600 ~/.linkedin/.env.

  4. Run the one-time OAuth flow:

    .venv/bin/python linkedin_oauth_setup.py

    This opens a browser for LinkedIn's consent screen, exchanges the resulting code for an access token, fetches the member's person URN, and writes both back into ~/.linkedin/.env.

  5. Register the server with Claude Code:

    claude mcp add linkedin-bridge --scope user -- \
      ~/git/claude-tools/.venv/bin/python ~/git/claude-tools/linkedin_bridge.py

Notes

  • Standard LinkedIn apps don't get a refresh token without extra approval — access tokens last ~60 days. Re-run linkedin_oauth_setup.py once one expires; post_to_linkedin will surface LinkedIn's own error message if a call is attempted with an expired token.

  • Uses urllib from the standard library rather than adding an HTTP client dependency, consistent with the rest of this repo's bridges.

  • Solved: the earlier "posts sometimes render truncated" issue was LinkedIn's "little" text format. The commentary field isn't plain text — it's a small markup language for mentions/hashtags, and characters reserved for that markup (( ) [ ] { } @ # < > \ * _ ~) must be backslash-escaped to appear as literal text. Every post logged during the original investigation was checked against this: every single one containing an unescaped ( or ) rendered truncated in the feed, every one without either character rendered in full — 11/11 with no exceptions. _escape_little_format() now escapes all reserved characters automatically before every post_to_linkedin/update_linkedin_post call, except #word sequences (left alone so intentional hashtags still render as hashtags). Confirmed fixed with a live test post containing parentheses.

  • update_linkedin_post/delete_linkedin_post don't need r_member_social (LinkedIn's read-back permission, currently closed for new access requests) - only reading a post back to verify its content needs that, so there's still no way to check a post's live content without a human looking at it.

  • Verified end-to-end: OAuth flow completed, a real post published successfully via post_to_linkedin, then updated via update_linkedin_post (tested with the full URL form) and deleted via delete_linkedin_post (tested with the bare URN form) - both input styles work.

  • The Linkedin-Version header is pinned to the previous month (_linkedin_api_version()), not the current one. LinkedIn versions its REST API by month, but a brand-new monthly version isn't always live in production right at the start of that month, requests using the current month's version have come back with a 426 NONEXISTENT_VERSION error, while the previous month's version is always already rolled out.

youtube-bridge

Exposes five tools for a solo-creator video pipeline: transcribe a raw recording, mechanically tighten it, cut it down semantically, then upload it to YouTube on a schedule.

  • transcribe_video(video_path) — local faster-whisper transcription with timestamps. Writes <video_path>.transcript.json and returns [MM:SS - MM:SS] text lines for Claude to read and reason about what to cut.

  • tighten_video(video_path, output_path="") — runs auto-editor for a mechanical first pass (cuts silence/dead air). Doesn't understand meaning — pair with cut_video for semantic cuts (flubbed takes, restarts, rambling).

  • cut_video(video_path, keep_segments, output_path="") — given a list of [start, end] second ranges to keep (picked by Claude from the transcript), re-encodes and concatenates via ffmpeg for frame-accurate cuts. Claude decides what to cut by reading the transcript; this tool only executes the mechanical trim.

  • queue_video_for_upload(video_path, title, description, tags, category_id="28", publish_at="") — uploads as privacyStatus: "private" with status.publishAt set, so YouTube itself flips the video public at that timestamp — no daemon or cron needed on this end. If publish_at is omitted, computes the next open slot from YOUTUBE_POST_TIMES (comma-separated HH:MM, local time — one entry = 1/day, two = 2/day), reading/advancing state in youtube_schedule_state.json so repeated calls in one batch-recording session spread out across days without double-booking. On success, moves the source file into a posted/ subfolder (date-prefixed) so it drops out of the pending queue.

  • list_pending_videos(folder="") — lists video files in folder (default YOUTUBE_VIDEO_DIR) not yet moved to posted/, i.e. what's left in a batch day's queue.

queue_video_for_upload schedules a video to go publicly live with no further confirmation at publish_at — always confirm title/description/ tags/timing with the user before calling it, never call it unprompted, same rule as linkedin-bridge.

Every call is logged to youtube_log.jsonl (gitignored).

Setup

  1. Install ffmpeg (brew install ffmpeg) and this project's Python dependencies (shared .venv, faster-whisper and auto-editor are in requirements.txt):

    cd ~/git/claude-tools
    .venv/bin/pip install -r requirements.txt
  2. Create a Google Cloud project, enable the YouTube Data API v3, and create an OAuth client of type Desktop app. Add http://localhost:8766/callback as an authorized redirect URI.

  3. Put the client credentials in ~/.youtube/.env:

    YOUTUBE_CLIENT_ID=...
    YOUTUBE_CLIENT_SECRET=...
    YOUTUBE_VIDEO_DIR=/path/to/your/raw-recordings-folder
    YOUTUBE_POST_TIMES=10:00,17:00

    then chmod 600 ~/.youtube/.env.

  4. Run the one-time OAuth flow (forces access_type=offline&prompt=consent so Google actually returns a refresh token):

    .venv/bin/python youtube_oauth_setup.py

    This writes YOUTUBE_REFRESH_TOKEN back into ~/.youtube/.env and prints the authorized channel name to confirm you authorized the right account.

  5. Register the server with Claude Code:

    claude mcp add youtube-bridge --scope user -- \
      ~/git/claude-tools/.venv/bin/python ~/git/claude-tools/youtube_bridge.py
  6. Restart Claude Code / reload the window.

Notes

  • Unlike LinkedIn's ~60-day access token, Google's refresh token doesn't expire from normal use — youtube_bridge.py exchanges it for a fresh access token on every call rather than caching one, so youtube_oauth_setup.py should only need to run once.

  • publishAt requires privacyStatus: "private" at upload time (YouTube rejects a scheduled public/unlisted upload) — the tool always sends "private", which is what triggers YouTube's own scheduling behavior.

  • Uses urllib from the standard library for OAuth and the resumable-upload protocol, consistent with the rest of this repo's bridges — no google-api-python-client dependency. The upload streams the video file from disk (a file object passed as data, not read_bytes()) so large recordings don't get fully buffered into memory; it's still a single PUT rather than chunked with per-chunk retry, which would matter more for very large files or flaky connections.

  • queue_video_for_upload rejects publish_at values less than 5 minutes out, and cut_video validates keep_segments (sorted, non-overlapping, end > start) before touching ffmpeg — both fail fast with a clear error instead of wasting an upload/encode on bad input.

  • _run() catches FileNotFoundError for missing binaries (ffmpeg, ffprobe, auto-editor) and returns a clean "not found — is it installed and on PATH?" error through the normal returncode-check path, instead of crashing the tool call with a raw traceback.

  • cut_video re-encodes at every cut (trim+concat filter graph) rather than stream-copying, trading some encode time for frame-accurate boundaries — stream-copy cuts only land on keyframes, which would make Claude's semantic cut points imprecise.

  • Default category_id is "28" (Science & Technology) — override per call if a video fits better under "27" (Education) or "26" (Howto & Style).

  • Scheduling logic (_next_publish_slot) verified standalone: queuing 5 videos in a row against YOUTUBE_POST_TIMES=10:00,17:00 produced 2026-07-18 10:00, 2026-07-18 17:00, 2026-07-19 10:00, 2026-07-19 17:00, 2026-07-20 10:00 — correct 2/day spread with no double-booking.

  • Full pipeline verified end-to-end 2026-07-18/19 (transcribe → tighten → queue → real OAuth upload), including three fixes found along the way:

    • tighten_video called bare auto-editor via subprocess.run, which only exists in this project's .venv/bin, not on the MCP server's inherited PATH. AUTO_EDITOR_BIN now resolves it explicitly (shutil.which first, falling back to the venv's own bin/ next to sys.executable).

    • macOS Screenshot/Screen Recording filenames insert a narrow no-break space (U+202F) before AM/PM, visually identical to a normal space but byte-different — any retyped (vs. copy-pasted) path silently failed Path.exists(). _resolve_video_path() now falls back to a whitespace-normalized filename match within the same directory across all four file-taking tools.

    • The first real upload flickered and had audio skips. Two compounding causes, both in tighten_video's default auto-editor invocation: (1) macOS screen recordings are variable-frame-rate, and left to its default auto-editor timed the output off the source's average fps, landing on an arbitrary non-standard rate (52.41fps); (2) auto-editor's default bitrate (~1.4Mbps observed) was far too low for a high-res (3024x1898) screen recording with sharp text, producing visible compression-artifact flicker. Fixing the frame rate alone did not resolve it — the low bitrate was the dominant cause. tighten_video now passes --frame-rate 60 (TIGHTEN_OUTPUT_FPS) and --video-bitrate 10M (TIGHTEN_VIDEO_BITRATE, comfortably above the source's own ~4Mbps). Caught only after publishing — a private/scheduled upload with a bad encode isn't something youtube-bridge can fix or delete itself (no update/delete tool exists yet, unlike linkedin-bridge); both bad uploads had to be deleted by hand in YouTube Studio before their scheduled publish time.

  • tighten_video disabled 2026-07-19, the fps/bitrate fix above did not actually resolve the flicker. Two more videos processed through the fixed tighten_video still showed the artifact and were unusable. Isolated the cause with an A/B test: the same recording, uploaded completely raw and unedited with no auto-editor pass at all, had no flicker. So the artifact comes from auto-editor's re-encode step itself, not the source capture and not YouTube's transcode, but the exact cause within auto-editor isn't identified yet. tighten_video now returns an explanatory error instead of running, rather than silently producing unusable output. transcribe_video, cut_video, and queue_video_for_upload are unaffected. Roadmapped below to revisit once the real cause is found.

  • Deliberately excluded from mcpo_config.json, same rationale as linkedin-bridge (see mcpo Notes below) — publishing tools stay MCP-only so the "confirm before calling" rule can't be bypassed by an HTTP client holding the proxy's API key.

slack-digest-bridge

Exposes one tool that fetches a Slack thread server-side and digests it with a local Ollama model, so the raw thread never has to land in the agent's own context the way it does through the hosted Slack connector:

  • digest_thread(thread_url, model="qwen2.5-coder:7b") — pass a Slack permalink to any message in the thread (a message's "Copy link" action) — works for both the parent message's link and a reply's link. Fetches the thread via conversations.replies and returns DECISIONS/ACTION_ITEMS/ BLOCKERS sections instead of the raw messages.

Every call is logged to slack_digest_log.jsonl (gitignored) as an audit trail.

Setup

  1. Create a Slack app at api.slack.com/appsCreate New AppFrom a manifest, pick the target workspace, and paste:

    display_information:
      name: claude-tools-digest
      description: Read-only bot for Slack thread digests via claude-tools
    features:
      bot_user:
        display_name: claude-tools-digest
        always_online: false
    oauth_config:
      scopes:
        bot:
          - channels:history
          - channels:read
          - groups:history
          - groups:read
    settings:
      org_deploy_enabled: false
  2. On the app's OAuth & Permissions page, click Install to Workspace and approve. Copy the Bot User OAuth Token (xoxb-...) shown after — ignore the separate App Credentials page (Client ID/Secret/Signing Secret); those are for building an external OAuth authorize flow, not needed for an app that only installs into your own workspace.

  3. Invite the bot to every channel you want digested — it can't read a channel's history until it's a member, even with the scopes granted: /invite @claude-tools-digest in each one.

  4. Put the token in ~/.slack/.env (create it yourself):

    SLACK_BOT_TOKEN=xoxb-...

    then chmod 600 ~/.slack/.env.

  5. Register the server with Claude Code:

    claude mcp add slack-digest-bridge --scope user -- \
      ~/git/claude-tools/.venv/bin/python ~/git/claude-tools/slack_digest_bridge.py
  6. Restart Claude Code / reload the window.

Notes

  • This exists because the hosted claude_ai Slack connector already returns raw thread content into the agent's context by the time the agent sees it — it can't be the fetch path for a tool meant to keep raw messages out of context, so this bridge holds its own bot token and fetches server-side instead.

  • Scopes are read-only on purpose (*:history/*:read, no chat:write) — this bot never posts, it only ever reads threads it's been invited to.

  • Message text can contain raw Slack IDs (<@U12345> for a user, <#C12345|name> for a channel) since the bot doesn't have users:read — the digest instructions tell the model to carry those through as-is rather than guess a display name.

  • conversations.replies is called with limit=200 and no pagination — if a thread has more than 200 replies, only the first batch is digested and the response says so. Not built out further since no thread in this workspace is anywhere near that size yet.

  • Verified end-to-end against a real message in #claude-tools (conversations.replies fetch, auth, and Ollama round trip all confirmed working), and separately against a synthetic 6-message thread (decision, action item with a raw <@U123> mention, a channel_join system message, and a bare :+1: reaction) to check the filtering and extraction logic: the join event and the reaction were correctly dropped, the decision and action item were both extracted correctly with the mention preserved verbatim. One real miss in that same test — an open question in the synthetic thread ("not sure if we need a separate channel for the mobile team") wasn't picked up as a BLOCKER. Same caveat as ollama-bridge's other triage tools: a 7B model's first pass, not a guarantee, re-read the thread yourself (slack_read_thread) for anything where a missed open question would actually matter.

image-bridge

Exposes one tool backed by Gemini's native image model (gemini-2.5-flash-image), reusing the same GEMINI_API_KEY as gemini-bridge:

  • generate_image(prompt, output_path, reference_image_paths=[]) — generates an image and saves it to output_path (absolute path, .png or .jpg). Pass reference_image_paths (absolute paths to existing images) to guide style, character likeness, and composition against those references, e.g. an existing book cover, so a series of illustrations actually looks like one book instead of restarting the style from scratch on every call. Returns the saved path on success.

Every call is logged to log.jsonl (shared with gemini-bridge, gitignored) as an audit trail.

Setup

  1. Same GEMINI_API_KEY as gemini-bridge (see its Setup section) — no separate credential needed.

  2. Install this project's dependencies (shared .venv, google-genai is in requirements.txt):

    cd ~/git/claude-tools
    .venv/bin/pip install -r requirements.txt
  3. Register the server with Claude Code:

    claude mcp add image-bridge --scope user -- \
      ~/git/claude-tools/.venv/bin/python ~/git/claude-tools/image_bridge.py
  4. Restart Claude Code / reload the window.

Notes

  • Uses the google-genai SDK directly (not the gemini CLI gemini-bridge shells out to) — the CLI has no image-generation flag, so this calls models.generate_content against gemini-2.5-flash-image itself, multimodal in: text prompt plus zero or more reference images as inline bytes, image out: the first inline_data part found in the response.

  • Blocked on billing as of 2026-08-06. The free tier of the API key in ~/.gemini/.env has a hard 0 request/token quota specifically for gemini-2.5-flash-image (confirmed via a live test call: clean HTTP round trip, clear 429 RESOURCE_EXHAUSTED response naming that exact quota) — every other quota on that key allows normal usage, this one model's free allowance is zero, not low. The code path itself is verified working end to end (request sent, error correctly parsed and returned, no image written), it just cannot succeed until billing is enabled on the Google AI Studio / Cloud project tied to that key.

  • No image was actually generated or shipped anywhere as of this note — everything above the Blocked line is verified-working plumbing, not a verified-working image.

discord-bridge

Exposes four tools for reading, posting to, and managing Discord text channels under a bot's own identity, via Discord's REST API:

  • list_channels(guild_id) — lists the text/announcement channels in a server, so you can find a channel's numeric ID from its name before calling the other tools.

  • read_channel(channel_id, limit=20) — returns the most recent messages (newest first), including attachment URLs. limit is capped at 100 (Discord's own per-request max).

  • post_message(channel_id, content) — posts a message under the bot's identity. Posts live and immediately, visible to everyone in the channel — always confirm the exact channel and text with the user before calling this, never call it unprompted, same rule as linkedin-bridge and youtube-bridge.

  • create_channel(guild_id, name, topic="") — creates a new text channel in a server. Requires the bot to have Manage Channels permission. Creates a real, visible channel immediately — always confirm the exact server and channel name with the user first, never call it unprompted, same rule as post_message.

Every call is logged to discord_log.jsonl (gitignored) as an audit trail.

Setup

  1. Create an app at discord.com/developers/applicationsNew Application. On the Bot tab, click Reset Token to reveal a bot token, and turn on the Message Content Intent toggle under Privileged Gateway Intents — without it, content comes back empty on messages the bot didn't author, even with the right channel permissions.

  2. On the OAuth2 → URL Generator page, check the bot scope, then under Bot Permissions check View Channels, Send Messages, Read Message History, and Manage Channels (needed for create_channel). Open the generated URL and invite the bot to your server. If the bot is already invited without Manage Channels, grant it the role permission directly in Server Settings → Roles instead of re-inviting.

  3. Put the token in ~/.discord/.env (create it yourself):

    DISCORD_BOT_TOKEN=...

    then chmod 600 ~/.discord/.env.

  4. Register the server with Claude Code:

    claude mcp add discord-bridge --scope user -- \
      ~/git/claude-tools/.venv/bin/python ~/git/claude-tools/discord_bridge.py
  5. Restart Claude Code / reload the window.

Notes

  • Built after searching for an existing Discord MCP server came up empty — the ones found were either read-only (webhook posting, no channel reading), abandoned, or required more setup than just standing up a minimal bridge in this repo's existing style. Same pattern as slack-digest-bridge: own bot token in a dotfile, stdlib urllib only, no new dependency in requirements.txt.

  • Uses Discord API version v10 (https://discord.com/api/v10), the current stable version as of this writing.

  • _format_discord_error parses Discord's JSON error body (message + numeric code) and adds a plain-English hint for the common failure modes: 401 (bad token), 403 (bot present but missing a permission, or Message Content Intent off), 404 (bot not actually in that server/channel, or a wrong ID).

  • Deliberately excluded from mcpo_config.json, same rationale as linkedin-bridge/youtube-bridgepost_message publishes visibly under a real identity, so it stays MCP-only where the "confirm before posting" rule is enforced by Claude Code's own tool-call flow, not bypassable by an HTTP client holding the proxy's API key.

  • list_channels has been verified against a live server. create_channel is new and not yet verified against a live server/bot with Manage Channels permission — code path mirrors the other tools' verified request/error-handling pattern, but treat it as unverified until run once for real.

github-audit-bridge

Exposes three tools for auditing and fixing Dependabot coverage across every repo on a GitHub account, backed by the gh CLI:

  • audit_dependabot_coverage(owner="wren-creator") — checks every non-archived repo under owner for vulnerability alerts, automated security-fix PRs, and a .github/dependabot.yml (scheduled version-update config), in parallel. Returns one compact table plus a "needs attention" line listing repos with alerts off.

  • list_open_work(owner="wren-creator") — lists open issues and open pull requests (fetched separately, not double-counted) for every repo that has any, skipping repos with nothing open.

  • enable_dependabot(owner, repo) — turns on vulnerability alerts and automated security-fix PRs for one repo. Idempotent, safe to call on a repo that already has it on. Does not create a dependabot.yml — that's a separate, per-repo config this tool doesn't write.

All three run entirely against the GitHub REST API through gh api — no LLM calls, no repo cloning. Built to replace looping raw gh api calls by hand for this kind of account-wide check, which burns a lot of turns doing the same handful of lookups per repo.

Setup

  1. gh auth login with a token scoped for repo security-events access (the account's existing gh auth, if already logged in, is reused as-is — no separate credential file).

  2. Register the server with Claude Code:

    claude mcp add github-audit-bridge --scope user -- \
      ~/git/claude-tools/.venv/bin/python ~/git/claude-tools/github_audit_bridge.py
  3. Restart Claude Code / reload the window.

Notes

  • Built 2026-08-10 after a manual 41-repo Dependabot audit for wren-creator burned a lot of turns looping gh api calls one repo at a time. Verified against that same account: audit_dependabot_coverage reproduced the manual audit's numbers exactly (36 repos on, 5 off), and enable_dependabot was used for real to turn on the 5 that were off.

  • vulnerability-alerts returns 204/exit 0 when on and 404/exit 1 when off — _check_repo_security reads that off gh's own returncode rather than parsing a body, since the endpoint has no JSON body either way. automated-security-fixes does return a body ({"enabled": true/false}), parsed instead of trusting the HTTP status — an earlier version of the by-hand audit that led to this tool mistakenly treated any 2xx as "enabled" and got it wrong for repos with fixes actually off.

  • .github/dependabot.yml presence is checked via the exit code of gh api repos/.../contents/..., not by inspecting stdout — gh api's 404 error body ({"message":"Not Found",...}) prints to stdout, not stderr, so an earlier draft that checked "is stdout non-empty" got a false "present" on every single repo, including ones with no config file at all. Caught by hand before this tool existed; the fix carried forward into _check_repo_security directly.

  • Excluded from mcpo_config.json on purpose, same rationale as linkedin-bridge/youtube-bridge/discord-bridgeenable_dependabot changes real settings on a live GitHub repo. Keeping it MCP-only means that change only ever happens through Claude Code's own guarded tool-call flow, not an HTTP client holding the proxy's API key.

playwright-bridge

Exposes eight tools backed by Playwright for driving a real browser mid-session, e.g. to visually verify a UI/CSS fix or confirm a network dependency is (or isn't) actually being hit:

  • launch(browser="chromium", headless=True, viewport_width=0, viewport_height=0) — starts a browser, returns a session_id. browser is "chromium", "firefox", or "webkit". Every request the page makes from this point until close() is recorded — see get_requests.

  • goto(session_id, url, wait_until="load") — navigates. wait_until is "load", "domcontentloaded", "networkidle", or "commit". Returns JSON {"url", "title", "status"}.

  • evaluate(session_id, script) — runs JavaScript in the page and returns the JSON-encoded result. script is an expression or function body, same as Playwright's own page.evaluate() — a script returning a Promise is automatically awaited. The general-purpose tool for reading DOM/computed style state or calling into a page's own JS (e.g. a dynamic import('/js/whatever.js')) that the other tools don't have a shape for.

  • screenshot(session_id, output_path, full_page=False, selector="") — saves a PNG to output_path (absolute path); read it back with Claude Code's own Read tool to view it. With selector set, screenshots just that element.

  • get_requests(session_id, url_contains="") — every network request made since launch(), as JSON [{"url", "method", "resource_type", "status"}, ...]. With url_contains set, filters to matching URLs — e.g. confirm a CDN dependency was actually removed by checking zero requests contain "fonts.googleapis.com", without needing devtools open.

  • click(session_id, selector, timeout_ms=5000) / fill(session_id, selector, text, timeout_ms=5000) — basic interaction, CSS or Playwright text=/role= selector syntax.

  • close(session_id) — closes the browser and frees its resources.

Sessions live in memory for the lifetime of the server process (one per Claude Code session) — close any session you're done with rather than letting it leak. Every call is logged to playwright_log.jsonl (gitignored).

Setup

  1. Install this project's dependencies (shared .venv, playwright is in requirements.txt):

    cd ~/git/claude-tools
    .venv/bin/pip install -r requirements.txt
  2. Install the browser binaries once — this is the actual fix for "waiting on a temporary install every session" (see Notes below). Start with Chromium alone; it covers most verification needs and, as of this writing, was already cached from earlier unrelated npx playwright usage, so it costs no download at all. Add Firefox/WebKit later (.venv/bin/playwright install firefox webkit) when on a connection that isn't metered/cellular — each is 100MB+ and this repo's own setup session stalled indefinitely trying to fetch Firefox over a cellular hotspot (see Notes):

    .venv/bin/playwright install chromium
  3. Register the server with Claude Code:

    claude mcp add playwright-bridge --scope user -- \
      ~/git/claude-tools/.venv/bin/python ~/git/claude-tools/playwright_bridge.py
  4. Restart Claude Code / reload the window.

Notes

  • Built 2026-08-11 after verifying a font-loading fix for web3270 required ad-hoc npx playwright install chromium firefox mid-session — Firefox's binary took over 20 minutes to fetch through that ephemeral path and never finished, while Chromium happened to already be cached from an earlier unrelated session. npx re-resolves and re-fetches playwright itself into a fresh ~/.npm/_npx/<hash>/ directory essentially every time it's invoked from a new working directory, so nothing about that install persists or speeds up the next one. A real dependency in this repo's shared .venv, installed once, does.

  • Browser binaries cache at ~/Library/Caches/ms-playwright/, keyed by browser version, not per-project — this is the same cache directory Node's playwright/@playwright/test packages use on the same machine, so a Chromium version already fetched by either language's tooling is reused rather than re-fetched. Installing here once covers this bridge permanently; it does not need to be redone per project or per session.

  • Uses Playwright's sync API (playwright.sync_api), not async_api — every tool function here is a plain synchronous function, consistent with the rest of this repo's bridges (subprocess-based, no asyncio event loop already running that would require the async variant instead).

  • get_requests exists specifically for the "did a network dependency actually get removed" class of check — the alternative (asking a human to open devtools, or scraping proxy/server logs) is slower and less precise than recording every request Playwright already sees pass through the page.

  • evaluate is deliberately the most general tool rather than adding narrower ones (e.g. a dedicated "check computed font-family" tool) — it covers arbitrary DOM/JS inspection including calling directly into a page's own ES module exports via a dynamic import(), which is what verifying the web3270 pipe-rendering fix actually needed (feeding a synthetic screen through the real client-side renderLiveScreen() function, not a reimplementation of it).

  • Not yet added to mcpo_config.jsonclick/fill can act on real, arbitrary web pages if pointed at one, closer to tn3270-bridge's send_keys (interacts with a live target) than a pure read tool, and there's no immediate need for HTTP-proxied access from a non-MCP harness. Revisit if that need comes up.

  • Verified end-to-end against a live web3270 Docker stack, calling this bridge's own functions directly (not just the ad-hoc Node/Playwright script that motivated building it): launch("chromium") started headless Chromium from the already-cached binary (no download), goto loaded the app and returned {"url", "title", "status": 200}, get_requests(..., url_contains="fonts.googleapis.com") correctly returned [], evaluate read back document.title, screenshot produced a real, correctly rendered PNG of the app UI, and close freed the session cleanly.

  • Getting a working .venv for this bridge was its own saga, worth recording since it's the exact pain this bridge exists to eliminate going forward: over a cellular hotspot connection, pip install playwright first failed outright after ~15 minutes with ConnectionResetError (silently — the driving command was piped through tail, so pip's own non-zero exit code was masked by tail's exit 0; don't trust a piped command's reported exit status for a slow install, check pip show <package> for a real Version: line instead), then a retry with --retries 10 --timeout 60 looked stuck for another ~15 minutes (same TCP connection, zero new data) before turning out to just be crawling at ~69 kB/s through a 42.5MB wheel (playwright's Python package bundles its own driver) — it finished on its own, unstuck, right as a second kill/retry was about to be triggered. Lesson: a slow-but- ESTABLISHED connection on a metered/cellular link can look identical to a dead one; check for forward progress (changing socket ports on retry, growing output) before killing and restarting something that's actually fine.

  • Firefox and WebKit are deliberately not installed yet — each browser binary is 100MB+, and the connection issues above make that an expensive thing to force on a cellular connection. playwright install firefox webkit (see Setup) is a same-day, low-risk follow-up once on a real connection; launch(browser="firefox"|"webkit") will fail with a clear Playwright "executable doesn't exist" error until then, not a silent wrong result.

mcpo proxy

Fronts gemini-bridge, tn3270-bridge, and repo-bridge with mcpo, so tool-calling harnesses that don't speak MCP natively (e.g. Ollama or llama.cpp-based agents) can call these tools over plain HTTP/OpenAPI instead. linkedin-bridge, youtube-bridge, discord-bridge, and github-audit-bridge are deliberately excluded - see each one's Notes.

Setup

  1. Install this project's dependencies (shared .venv, mcpo is in requirements.txt):

    cd ~/git/claude-tools
    .venv/bin/pip install -r requirements.txt
  2. Run it, pointing at mcpo_config.json and picking a real API key (not the placeholder below):

    .venv/bin/mcpo --port 8000 --api-key "your-own-key-here" --config mcpo_config.json
  3. Each server's tools are now live at http://localhost:8000/<server-name>/<tool-name> (e.g. http://localhost:8000/repo-bridge/list_structure), with interactive OpenAPI docs per-server at http://localhost:8000/<server-name>/docs and a combined spec at http://localhost:8000/openapi.json. Every actual tool call requires Authorization: Bearer <api-key>; the docs/spec endpoints themselves are intentionally public (mcpo's default behavior).

Notes

  • linkedin-bridge is excluded from mcpo_config.json on purpose. post_to_linkedin's "always confirm the exact text with the user first" rule is something Claude Code follows as an instruction, not something the proxy enforces - any HTTP client holding the --api-key could otherwise trigger a real, public LinkedIn post with no confirmation step. Keeping it MCP-only means posting only ever happens through Claude Code's own guarded tool-call flow.

  • Verified end-to-end: started mcpo with all three servers, confirmed a real list_structure call succeeds with the API key and returns 401 without it, and confirmed the (intentionally public) openapi.json//docs endpoints are reachable either way.

ollama-agent

A standalone chat loop (ollama_agent.py, not an MCP server itself) that lets a local Ollama model actually call the tools mcpo fronts - gemini-bridge, tn3270-bridge, repo-bridge - over plain HTTP. The DIY alternative to routing through Open WebUI: no extra service, just a script that turns mcpo's OpenAPI spec into Ollama's own function-calling tools format and dispatches whatever the model calls.

  • Fetches each server's own openapi.json from mcpo (there's no single combined spec with real paths - see Notes) and converts it into Ollama's tools format.

  • Runs a normal /api/chat loop: sends the conversation plus tools, executes any tool call against mcpo, feeds the result back, repeats until the model gives a plain-text final answer (capped at 8 tool-call rounds to avoid a runaway loop).

  • Falls back to parsing a JSON-shaped call out of message.content when the model answers with one instead of populating Ollama's structured tool_calls field - see Notes, this is qwen2.5-coder's actual default behavior locally, not a rare edge case.

Setup

  1. Have mcpo already running against mcpo_config.json (see the mcpo proxy section above):

    .venv/bin/mcpo --port 8000 --api-key "your-key" --config mcpo_config.json
  2. Put the same key mcpo was started with in ~/.mcpo/.env (create it yourself):

    MCPO_API_KEY=your-key
    MCPO_URL=http://localhost:8000

    then chmod 600 ~/.mcpo/.env.

  3. Run it:

    .venv/bin/python ollama_agent.py                       # interactive REPL
    .venv/bin/python ollama_agent.py "your one-shot prompt"

    --model (default qwen2.5-coder:7b), --mcpo-url, --api-key, and --num-ctx (default 8192) override the defaults/dotfile. --list-tools prints the loaded tools and exits without calling Ollama; the same list is available mid-conversation via /tools, in either interactive mode or as the one-shot prompt.

Every tool call (name, arguments, result) is logged to ollama_agent_log.jsonl (gitignored).

Notes

  • mcpo's combined /openapi.json is just an index page linking to each server's own docs - its paths object is empty. The real per-tool schemas live at /<server-name>/openapi.json, one FastAPI sub-app per MCP server. fetch_mcpo_tools reads mcpo_config.json directly for the server name list (the same config mcpo itself was started with) instead of trying to discover servers from the index page, then fetches each server's own spec and resolves its $ref schemas.

  • Confirmed live 2026-08-11 against both qwen2.5-coder:7b and :14b: neither actually populates Ollama's structured message.tool_calls field, despite both reporting tools in their ollama show capabilities - they answer with a bare {"name": ..., "arguments": {...}} JSON object in message.content instead, sometimes with more than one such object printed back to back as plain text rather than one valid JSON value. _fallback_tool_calls scans content for every { and tries json.JSONDecoder().raw_decode() from there, which stops at the first balanced close-brace and ignores anything before/after - handles markdown fences, multiple sequential calls, and trailing commentary without special-casing any of them.

  • Also seen live: the model dropping a tool's <server>__ prefix (e.g. calling get_file instead of the registered repo-bridge__get_file). _fallback_tool_calls resolves a bare name back to its full one when exactly one server exposes it, and stays silent (falls through as unrecognized) rather than guessing when a bare name is ambiguous across servers.

  • The system prompt explicitly warns against placeholder paths like /path/to/repo - without it, a prompt that didn't spell out the repo path verbatim got a hallucinated placeholder path passed straight to a real tool call. Always state the real absolute path in the prompt; the warning reduces but doesn't eliminate this.

  • num_ctx is set explicitly to 8192, same reasoning as ollama_bridge.py's _call_ollama - Ollama defaults to 2048 regardless of a model's real context length, which would silently truncate the tool schemas and system prompt before the conversation even starts.

  • :14b ran noticeably slower than :7b on this (CPU-only, no local GPU) hardware - :7b is the default for interactive use; pass --model qwen2.5-coder:14b when latency isn't a concern.

  • Verified end-to-end 2026-08-11: repo-bridge__list_structure and repo-bridge__get_file, both via qwen2.5-coder:7b against a live mcpo instance fronting this repo's own three servers - real tool calls dispatched, real results fed back, and a correct final answer synthesized from the actual file contents (DEFAULT_MODEL = "qwen2.5-coder:7b" read back out of ollama_bridge.py itself).

  • Same exclusions as the mcpo proxy section above - linkedin-bridge/youtube-bridge/discord-bridge/github-audit-bridge aren't in mcpo_config.json, so this script can't call them either.

Roadmap

  • Add a third gemini-bridge tool for querying Gemini's larger context window on full files, not just diffs.

  • repo-bridge: expand get_symbol language support beyond python/javascript/typescript/tsx/go — added rust, java, ruby, c, and cpp.

  • repo-bridge: get_symbol should return all matches instead of just the first.

  • tn3270-bridge: structured read_screen mode (field positions, protected/unprotected, cursor location) alongside the plain-text dump, for when an agent needs to know where to type, not just what's shown.

  • Front the MCP servers with an mcpo (MCP-to-OpenAPI) proxy so tool-calling harnesses built on Ollama or llama.cpp — which don't speak MCP natively — can call these tools over plain HTTP. linkedin-bridge excluded on purpose (see mcpo Notes).

  • linkedin-bridge: dig further into the post-truncation issue — root caused to LinkedIn's "little" text format (see Notes above); fixed by auto-escaping reserved characters before every post/update.

  • linkedin-bridge: add update_linkedin_post / delete_linkedin_post — both work with the w_member_social scope this app already has.

  • Add youtube-bridge: transcribe/tighten/cut a raw recording, then queue it for scheduled upload (YouTube's own publishAt, no daemon) with source files auto-moved to posted/. Upload/OAuth path verified end-to-end with a real upload (see Notes above).

  • youtube-bridge: add update_youtube_video / delete_youtube_video (mirroring linkedin-bridge's pattern). Surfaced 2026-07-19 when a bad first upload (flicker/audio-skip from the fps bug, see Notes) sat privately scheduled on YouTube with no way to remove or replace it via MCP — had to be fixed by hand in YouTube Studio.

  • youtube-bridge: find the actual cause of tighten_video's flicker/ scanline artifact and re-enable it. Confirmed 2026-07-19 the fps/ bitrate fix didn't fix it, and that the artifact is specific to auto-editor's re-encode (raw unedited upload of the same recording had no artifact), but not yet which part of auto-editor's pipeline is at fault. Currently disabled, returns an error instead of running.

  • linkedin-bridge: scheduled posting. LinkedIn's API has no server-side scheduled publish for personal profiles (that's a Company Page / Campaign Manager feature), so this would need Claude-side scheduling (a cron routine calling post_to_linkedin at a set time) with pre-approved text, mirroring youtube-bridge's publishAt pattern but without native platform support. Surfaced 2026-07-21.

  • youtube-bridge: chunked resumable upload with retry, for large files or flaky connections (current version sends the whole video in one PUT).

  • youtube-bridge: thumbnail upload (thumbnails.set) once the core transcribe → cut → schedule path is verified end-to-end.

  • linkedin-bridge: add read-back support (a tool that fetches a post's live content) once r_member_social is available. Blocked on LinkedIn, no ETA — that permission is currently closed to all new access requests, not just under heavy review, so there's nothing to do here until that changes.

  • Add ollama-bridge: a prefilter_diff tool backed by a local Ollama model (qwen2.5-coder:7b), so routine diffs get a free/offline triage pass before spending a review_diff (Gemini) call — only escalate when it comes back FLAGGED. Verified end-to-end 2026-07-24.

  • ollama-bridge: add triage_log, a second tool that extracts just the root failure (FILE:/ERROR:/CONTEXT:) from a build/test log file via the same local model, instead of reading the whole raw log. Verified end-to-end 2026-07-24 against synthetic pytest logs (one failing, one clean).

  • tn3270-bridge: panel-state abstractor. Added read_panel_state, which extracts IBM message-ID lines (ICH408I, etc.) and unprotected fields with best-guess labels instead of a full screen dump. Surfaced 2026-08-06 brainstorming session on cutting agent token load. Unit tested against synthetic screen data only, not yet verified against a live host (see Notes above) — no test mainframe was reachable in this environment.

  • ollama-bridge: add triage_transcript, flagging likely restarts/ filler/dead-air spots in a youtube-bridge transcript instead of the agent reading the whole thing closely to find them. Doesn't replace transcribe_video's full transcript (cut_video's docstring is explicit that cut decisions need the agent reading the real thing), it points at where to look first. Surfaced 2026-08-06, ranked #2. Verified end-to-end against a synthetic transcript with a real Ollama call.

  • image-bridge: blocked on billing. Built generate_image against Gemini's native image model (gemini-2.5-flash-image), code path verified end-to-end (clean request, correctly parsed 429 RESOURCE_EXHAUSTED error), but the free-tier API key has a hard 0 quota for that specific model. Needs billing enabled on the Google AI Studio / Cloud project before a real image can be generated. Surfaced 2026-08-06 while building interior illustrations for a children's book manuscript.

  • New slack-digest-bridge: digest_thread, a local-model digest of a Slack thread (decisions/action-items/blockers) fetched server-side via its own read-only bot token, so raw thread content never has to land in agent context. Surfaced 2026-08-06, ranked #3. Verified end-to-end against a real message in #claude-tools and a synthetic multi-message thread (see Notes above) — one real miss found: an open question wasn't flagged as a BLOCKER, documented as a known first-pass gap.

  • New discord-bridge: list_channels/read_channel/post_message for a Discord server, since no existing Discord MCP server actually covered both reading and posting. Surfaced 2026-08-07. Not yet verified against a live server (see Notes above) — no Discord bot token was available to test against in this environment.

  • Created #britleys-corner on the Discord server (guild 961749187802824724) as a project idea inbox, random thoughts and ideas land there before they earn the right to become a real project. Introduced 2026-08-07.

  • Measured the actual token savings from the prefilter_diff escalation order, pulled straight from ollama_log.jsonl: 149 diffs reviewed by the local model, 147 of them (98.7%) came back clean and never escalated to review_diff, only 2 needed the bigger model. That's roughly 90,000+ tokens of review output avoided on diff review alone. Surfaced 2026-08-08 while drafting a LinkedIn post on tiering AI work by cost, numbers cited there are these.

  • New github-audit-bridge: audit_dependabot_coverage/list_open_work/ enable_dependabot for checking and fixing Dependabot coverage across every repo on a GitHub account via gh api, no LLM calls. Surfaced 2026-08-10 after a manual 41-repo audit burned a lot of turns looping gh api by hand. Verified end-to-end against the real wren-creator account — reproduced the manual audit's numbers exactly, and enable_dependabot was used for real to fix the 5 repos found off.

  • New playwright-bridge: launch/goto/evaluate/screenshot/ get_requests/click/fill/close for driving a real browser mid-session — visual verification, computed-style/DOM inspection, and confirming a network dependency is or isn't actually being hit. Surfaced 2026-08-11 verifying a web3270 font-loading fix, where an ad-hoc npx playwright install chromium firefox stalled on the Firefox binary for 20+ minutes and never finished — nothing about that install persists between sessions, so the next session would hit the same wall. A real .venv dependency, installed once, does persist (see Notes for the install itself turning into its own saga over a cellular hotspot). Verified end-to-end against a live web3270 Docker stack: launch+goto loaded the app, get_requests correctly confirmed zero fonts.googleapis.com requests, evaluate and screenshot both round-tripped real data. Chromium only for now — Firefox/WebKit installs deferred pending a non-metered connection.

  • New ollama_agent.py: a standalone chat loop giving a local Ollama model real tool access to the tools mcpo fronts (gemini-bridge, tn3270-bridge, repo-bridge), the DIY answer to "how can Ollama call these bridges" without routing through Open WebUI. Surfaced 2026-08-11. Verified end-to-end against a live mcpo instance and qwen2.5-coder:7b, including two real model quirks caught and handled along the way: neither :7b nor :14b populate Ollama's structured tool_calls field, they answer with JSON text in message.content instead, and the model sometimes drops a tool's <server>__ prefix or invents a placeholder path on an underspecified prompt. See the ollama-agent section above for the fixes for each.

A
license - permissive license
Not graded
quality - not tested
B
maintenance

Maintenance

Maintainers
Response time
Release cycle
Releases (12mo)
Commit activity

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Related MCP Servers

View all related MCP servers

Related MCP Connectors

  • Hosted MCP server connecting claude.ai, ChatGPT and other AI apps to your own computer

  • Augments MCP Server - A comprehensive framework documentation provider for Claude Code

  • Hosted Amazon Seller and Vendor MCP server for Claude, ChatGPT, Cursor, Codex, Gemini, Copilot.

View all MCP Connectors

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/wren-creator/claude-tools'

If you have feedback or need assistance with the MCP directory API, please join our Discord server