workbenchBLOCK
One MCP endpoint. Every tool your agent needs. Self-hosted, per-user OAuth. 20+ SaaS integrations and invite you to add more.
Overview
From the repository's own README, as read at the audited commit. Badges and raw HTML are left out.
Self-hosted MCP tool aggregator. One endpoint, per-user OAuth, 194 tools across 16 integrations — behind 9 meta-tools.
**Website** · Documentation · Quickstart · Integrations · Build a plugin · Deploy
[](https://github.com/barockok/workbench/actions) [](https://codecov.io/gh/barockok/workbench) [](LICENSE)
Give an agent one MCP endpoint instead of sixteen. workbench holds a separate OAuth connection per user per provider, encrypts every token at rest, and exposes every integration through a fixed set of nine meta-tools — so the agent's tool list stays the same size whether one integration is connected or all of them.
flowchart LR
Agent["Agent"] -->|JSON-RPC| MCP["POST /mcp"]
Script["Script / cron"] -->|plain JSON| REST["POST /rest/:integration"]
MCP --> Meta["9 meta-tools"]
Meta --> Reg["Plugin registry16 integrations · 194 tools"]
REST --> Reg
Reg -->|credential injected| APIs["Jira · GitHub · Slack · Google · ..."]
Portal["Portal"] --> Store[("Encrypted tokens")]
Meta -.-> StoreAnything that is not an agent can skip MCP entirely: POST /rest/:integration runs the same tools over plain JSON, on the same credentials, with no JSON-RPC framing and no 60,000-character result cap.
Quickstart
7477d052587dOBSERVED · 2026-10-13Connect
Built from this server's own package name, version and transport as found in its source — not copied from anyone's documentation, so it cannot drift against a page we do not control. Replace the environment placeholders with a token scoped to the least it needs.
claude mcp add site --env CAPTURE_PROXY_PASSWORD=${CAPTURE_PROXY_PASSWORD} --env GITHUB_CLIENT_SECRET=${GITHUB_CLIENT_SECRET} --env GITLAB_CLIENT_SECRET=${GITLAB_CLIENT_SECRET} --env GOOGLE_GMAIL_CLIENT_SECRET=${GOOGLE_GMAIL_CLIENT_SECRET} -- npx -y @a-workbench/[email protected]{
"mcpServers": {
"site": {
"command": "npx",
"args": [
"-y",
"@a-workbench/[email protected]"
],
"env": {
"CAPTURE_PROXY_PASSWORD": "${CAPTURE_PROXY_PASSWORD}",
"GITHUB_CLIENT_SECRET": "${GITHUB_CLIENT_SECRET}",
"GITLAB_CLIENT_SECRET": "${GITLAB_CLIENT_SECRET}",
"GOOGLE_GMAIL_CLIENT_SECRET": "${GOOGLE_GMAIL_CLIENT_SECRET}"
}
}
}
}Exposed tools (200)
140 read · 87 write · 9 destructive. Blast radius: 9 tools can delete or overwrite — an agent that can be talked into calling a tool can be talked into calling this one.
| Tool | Risk | Description |
|---|---|---|
acme_cfg_search | read | x |
acme_create | write | Create an issue |
acme_list | read | list |
acme_off_search | read | Search acme records |
acme_search | read | Search issues |
alpha_tool | read | Alpha |
asana_add_comment | write | Add a comment (story) to an Asana task. Plain text only. Returns { gid, created_at } of the new comment. Use this for status updates or context on an existing task — to change task fields themselves use asana_update_task instead. |
asana_create_task | write | Create a new Asana task in a project. Returns the created task including its gid — keep the gid to update, complete (asana_update_task with completed:true), or comment on it later. dueOn is YYYY-MM-DD; assignee is a user gid (find one with asana_search_users). |
asana_list_projects | write | List Asana projects as slim rows: { gid, name } (default 10, set limit for more). Scope to one workspace via workspaceId, otherwise all workspaces you can see. Use the gid with asana_list_tasks / asana_create_task. |
asana_list_tasks | read | List tasks in an Asana project as slim rows: { gid, name, completed, due_on, assignee } (default 10, set limit for more). Use asana_get_task on a gid when you need notes or the permalink. Find project gids with asana_list_projects. |
asana_list_teams | write | List teams in an Asana organization as slim rows: { gid, name } (default 10, set limit for more). organizationId is the workspace gid of an organization. |
asana_search_users | read | Search Asana users in a workspace as slim rows: { gid, name, email } (default 10, set limit for more). Omit query to list everyone. Use the gid as assignee / assigneeGid in asana_create_task and asana_update_task. |
beta_tool | read | Beta |
bitbucket_add_pr_comment | write | Add a comment to a pull request. Plain |
bitbucket_approve_pr | read | Approve a pull request as the connected user (the green checkmark reviewers see). Non-destructive and revocable in the Bitbucket UI. Review the changes with bitbucket_get_pr_diff before approving. Returns { approved: true }. |
bitbucket_create_pr | write | Create a Bitbucket pull request from sourceBranch into destinationBranch (default \ |
bitbucket_decline_pr | read | Decline (reject) a pull request. WARNING: this closes the PR without merging — the author must open a new PR to resurrect it. Prefer bitbucket_request_changes if you only want revisions. Returns the PR |
bitbucket_get_file | read | Read a file |
bitbucket_get_pipeline | read | Get a single Bitbucket pipeline by uuid (or build number) as a slim object { uuid, build_number, state, result, ref_name, created_on, completed_on }. Use to poll a pipeline you triggered (state: PENDING | IN_PROGRESS | COMPLETED; result: SUCCESSFUL | FAILED | STOPPED). workspace defaults to the user |
bitbucket_get_pr | read | Get one pull request: title, description, state, author, source/destination branches, reviewers with approval status, comment/task counts, timestamps, and html URL. Always check this (state, approvals, open tasks) before bitbucket_merge_pr. For the code changes use bitbucket_get_pr_diff. |
bitbucket_get_repo | read | Get one Bitbucket repository: slug, name, full_name, privacy, main branch, last update, description, and the https clone URL. Use after bitbucket_list_repos when you need the clone URL or to confirm the main branch before creating a PR. |
bitbucket_list_default_reviewers | read | List the default reviewers that will auto-attach to new pull requests on a repository. Returns slim rows { display_name, uuid, reviewer_type: |
bitbucket_list_pr_comments | read | List comments on a pull request as slim rows: { id, author, text, inline: { path, line } for code comments (absent for general comments), created_on }. pagelen defaults to 20. Returns { comments, hasMore }. Use before replying with bitbucket_add_pr_comment to see existing review feedback. |
bitbucket_list_pr_commits | read | List the commits in a pull request as slim rows: { hash (12 chars), message (first line only), author, date }. Cheaper than the diff for understanding the shape/history of a PR. Returns { commits, hasMore }. |
bitbucket_list_prs | read | List pull requests in a repository as slim rows (id, title, state, author, source/destination branch, updated_on). |
bitbucket_list_repos | read | List repositories in a Bitbucket workspace as slim rows (slug, name, full_name, is_private, main branch, updated_on, description) — no links/avatars/clone noise. |
bitbucket_merge_pr | write | Merge a pull request into its destination branch. IRREVERSIBLE: this lands the commits on the destination branch — check state, approvals, and open tasks with bitbucket_get_pr first, and only merge when explicitly asked to. Optional: |
bitbucket_request_changes | read | Mark a pull request as \ |
bitbucket_search_users | read | Search Bitbucket workspace members by name or username to resolve user UUIDs for use with bitbucket_create_pr reviewers. Substring match against display_name OR nickname (case-insensitive) — |
bitbucket_stop_pipeline | write | Stop a running Bitbucket pipeline by uuid. Returns { ok: true } on success (204 No Content); on failure returns Bitbucket |
bitbucket_trigger_pipeline | write | Trigger a Bitbucket Pipelines run on a branch ( |
browser_audio_start | write | Give this tab a live audio pipe so you can take part in a browser call (Zoom web, Slack huddle, Meet, Jitsi). |
browser_audio_stop | write | End the audio pipe on this tab: the SSE stream gets |
browser_await_download | read | Wait for the download armed by browser_expect_download to finish, and return { name, bytes, expiresAt } for the file now in your workspace. Read it with files_read, hand it on with files_presign, or upload it into another page with browser_upload_file. |
browser_click | read | Click at viewport coordinates (x, y) in this tab. |
browser_close | read | Close this tab. The browser and its logged-in profile stay; other tabs are untouched. An idle browser closes itself after BROWSER_SESSION_TTL_SECONDS. |
browser_evaluate | write | Run JavaScript in the page and return its value — the page.evaluate of this toolset. Use it for anything coordinates cannot express: click by selector (document.querySelector( |
browser_key | read | Press a key or chord, e.g. |
browser_live_url | read | Get a short-lived URL to watch and take over your browser in a web canvas. Open it to drive the same browser by hand, then return control to the model. |
browser_navigate | read | Navigate this tab to a URL. Returns the final url and page title. |
browser_read_text | read | Read the visible text of the current page (document.innerText) as plain text — far cheaper than a screenshot for text-heavy pages, forms, and reading. Use this instead of browser_screenshot when you don |
browser_screenshot | read | Shot |
browser_scroll | read | Scroll the viewport up/down/left/right by an optional pixel amount (default 600). |
browser_start | write | Open a new tab in your browser and return its session_id. Pass it to every other browser_* call. Call it once per independent task; two agents each get their own tab and never step on each other. All tabs share one browser, so a login in one is visible in the others. |
browser_tabs | read | List the tabs open in your browser: session_id, url, title, and whether this toolset can drive it (a popup the page opened is listed but not driveable). |
browser_type | read | Type text into the currently focused element. Click the field first. |
browser_upload_file | write | Put a file from your workspace into a file input on the current page. |
clevertap_get_campaign_report | read | Get delivery and engagement report for a campaign by numeric ID. |
clevertap_get_campaigns | read | List campaigns within date range (id, name, scheduled_on, status). ${YYYYMMDD_DESC} |
clevertap_get_dau | read | Get Daily Active Users trend (unique App Launched) for date range. ${YYYYMMDD_DESC} |
clevertap_get_event_count | read | Count users who performed an event within date range. ${YYYYMMDD_DESC} ${SYSTEM_EVENTS_INFO} ${CQL_INFO} Supports property filters (event_properties, session_properties, common_profile_properties, advanced_query). Auto-polls if async. |
clevertap_get_event_trend | read | Get daily/weekly/monthly trend for an event. Supports unique count and sum of numeric property. ${YYYYMMDD_DESC} ${SYSTEM_EVENTS_INFO} ${CQL_INFO} Auto-polls if async. |
clevertap_get_events | read | Query event data for an event within a date range. ${YYYYMMDD_DESC} ${SYSTEM_EVENTS_INFO} ${CQL_INFO} Returns cursor for paginated results — use clevertap_get_events_cursor for next pages. |
clevertap_get_events_cursor | read | Fetch next page of event results using cursor from clevertap_get_events. |
clevertap_get_message_report | read | Get delivery/engagement report (sent, delivered, opened, clicked) filtered by channel, delivery type, status, label. ${YYYYMMDD_DESC} |
clevertap_get_profile | read | Retrieve a user profile by identity, email, or objectId (GUID). Provide at least one. ${PREDEFINED_PROFILE_INFO} |
clevertap_get_profile_count | read | Count profiles who performed an event within date range. ${YYYYMMDD_DESC} ${SYSTEM_EVENTS_INFO} ${CQL_INFO} Supports property filters (event_properties, session_properties, common_profile_properties, advanced_query). Auto-polls if async. |
clevertap_get_profiles_by_event | read | Get profiles of users who performed an event within date range. ${YYYYMMDD_DESC} ${SYSTEM_EVENTS_INFO} ${PREDEFINED_PROFILE_INFO} ${CQL_INFO} Returns cursor — use clevertap_get_profiles_cursor for next pages. |
clevertap_get_profiles_cursor | read | Fetch next page of profiles using cursor from clevertap_get_profiles_by_event. |
clevertap_get_real_time_counts | read | Get count of users active right now (last 5 minutes). Optionally breakdown by user type. |
clevertap_get_top_property_count | read | Get top property value counts for an event (e.g. top product categories). ${YYYYMMDD_DESC} ${SYSTEM_EVENTS_INFO} ${CQL_INFO} Auto-polls if async. |
clevertap_get_uninstall_report | destructive | Get uninstall count trend for date range. ${YYYYMMDD_DESC} |
clevertap_list_projects | read | List all configured CleverTap projects (name, account ID, region). |
clevertap_poll | read | Poll async CleverTap result via req_id until success/fail. |
clevertap_set_projects | write | Set all CleverTap projects at once via JSON. Overwrites existing. Use for multi-account bulk config. Input is JSON array: [{\ |
confluence_create_page | write | Create a Confluence page in a space. body is Confluence storage format (XHTML) — plain text works for simple pages. Pass parentId to nest under an existing page. spaceKey is resolved to the numeric spaceId internally; pass spaceId directly to skip the lookup. Returns the new page |
confluence_delete_page | destructive | Delete a Confluence page by ID (moves it to space trash). Irreversible via this API. |
confluence_get_page | read | Get a Confluence page by ID, including its body (storage XHTML), version number, and space. Returns { id, title, spaceKey, version, body, url, attachments }. |
confluence_list_spaces | read | List Confluence spaces (up to |
confluence_search_pages | read | Search Confluence pages. Pass |
confluence_update_page | write | Update a Confluence page |
delete_jot | destructive | Delete a jot you own by name. Returns FORBIDDEN if another user owns it, NOT_FOUND if it doesn |
deploy_jot | write | Begin deploying a static web artifact to /j/<name>/. Returns an upload URL and a single-use token (valid ~5 min). Package your site directory as a gzip tarball and upload it, e.g.: |
exec_tool | write | Exec |
execute_tools | write | Execute one or more tools in a single call. Runs them concurrently (bounded) and returns a |
files_delete | destructive | Delete a file from your workspace. Files expire on their own after 24 hours; this is for reclaiming quota early. |
files_list | read | List the files in your workspace — the per-user scratch area that browser downloads land in and that uploads are read from. Returns name, bytes, mtime and expiresAt for each. ${RETENTION} |
files_presign | read | Mint a short-lived URL for one workspace file, so something that cannot hold a workbench credential can fetch or write it — a service that ingests by URL, an upload target, or a transfer that should not pass through your context. op |
files_read | read | Read a workspace file. Use encoding |
files_stat | read | Get one workspace file |
files_write | write | Write a file into your workspace, so it can be uploaded into a page with browser_upload_file or handed to another integration. Use encoding |
get_tool_schema | read | Get input schema for a specific tool |
github_add_issue_comment | write | Add a comment to a GitHub issue (also works as a PR discussion comment). Returns { id, html_url }. For inline code-review comments on a PR diff, use github_add_pr_comment with type: |
github_add_pr_comment | write | Comment on a pull request. type: |
github_cancel_workflow_run | write | Cancel an in-progress GitHub Actions workflow run by id. Returns { ok: true } on success; on failure returns GitHub |
github_create_issue | write | Create a GitHub issue with optional body and labels. Returns the created issue. Follow up with github_update_issue / github_add_issue_comment; use github_list_issues to find existing ones first and avoid duplicates. |
github_create_or_update_file | write | Create or update a single file in a GitHub repo with a commit message (content is plain text, base64-encoded automatically). Updating an existing file REQUIRES its current sha — fetch it with github_get_content first or the call fails with 409/422. Defaults: branch=main. |
github_create_pr | write | Open a GitHub pull request from head branch into base branch. Returns the created PR. Follow up with github_get_pr / github_get_pr_diff to review it and github_merge_pr to land it. |
github_create_pr_review | write | Submit a formal review verdict on a pull request: event APPROVE, REQUEST_CHANGES, or COMMENT, with an optional summary body. Returns { id, state }. For a single inline or discussion comment without a verdict, use github_add_pr_comment instead. |
github_get_content | read | Read a file (base64 content + sha) or list a directory in a GitHub repo at an optional ref. Returns the file |
github_get_issue | read | Get one GitHub issue: { number, title, body (truncated to 2000 chars), state, author, labels, assignees, comments, html_url }. Use github_list_pr_comments-style follow-ups via github_add_issue_comment to reply. |
github_get_pr | write | Get one pull request: title, body (truncated to 2000 chars), state, merged/mergeable, author, head/base refs, draft, additions/deletions/changed_files, comment counts, html_url. Use github_get_pr_diff for the actual code changes. |
github_get_pr_diff | read | Read a pull request |
github_get_repo | read | Get one GitHub repository |
github_get_workflow_run | write | Get a single GitHub Actions workflow run by id as a slim object { id, name, head_branch, event, status, conclusion, run_started_at, html_url }. Use this to poll a run you triggered with github_trigger_workflow (status: queued | in_progress | completed; conclusion: success | failure | cancelled | ... |
github_list_branches | read | List branches in a GitHub repository as slim rows { name, protected }. Defaults: 30 per page. Use github_list_commits to see history on a branch. |
github_list_commits | read | List commits in a GitHub repository as slim rows { sha (12 chars), message (first line), author, date }, newest first. Pass sha to start from a branch/commit. Defaults: 30 per page. |
github_list_issues | read | List issues in a GitHub repository as slim rows { number, title, state, author, labels, assignee, comments, updated_at, is_pr }. Note: GitHub |
github_list_pr_comments | read | List comments on a pull request as slim rows { id, author, body, path?, line?, created_at }. type: |
github_list_prs | read | List pull requests in a GitHub repository as slim rows { number, title, state, author, head, base, draft, updated_at }. Defaults: state=open, 10 per page. Use github_get_pr for one PR |
github_list_releases | read | List releases for a GitHub repository as slim rows { tag_name, name, draft, prerelease, published_at }, newest first. Defaults: 30 per page. |
github_list_repos | read | List the authenticated user |
github_list_workflow_runs | read | List GitHub Actions workflow runs (CI status) as slim rows { id, name, head_branch, event, status, conclusion, run_started_at, html_url }, newest first. Optional branch and status (e.g. completed, in_progress, failure) filters. Default 10 per page. |
github_merge_pr | write | Merge a pull request into its base branch. IRREVERSIBLE — this lands the code; confirm the right PR number first (github_get_pr) and prefer reviewing via github_get_pr_diff before merging. Optional merge_method (merge|squash|rebase) and commit_title. |
github_rerun_workflow_run | write | Re-run a GitHub Actions workflow run by id (all jobs). Pass failedOnly to re-run only the failed jobs (rerun-failed-jobs). Returns { ok: true } on success; on failure returns GitHub |
github_search_code | read | Search code across GitHub as slim rows { path, repository, html_url }. Pass full search qualifiers in q (e.g. |
github_search_issues | read | Search issues and PRs across GitHub as slim rows { number, title, state, author, labels, comments, updated_at, is_pr, repository }. Pass full qualifiers in q (e.g. |
github_trigger_workflow | write | Trigger a GitHub Actions workflow run via workflow_dispatch. |
github_update_issue | write | Update a GitHub issue — set any of title, body, state (open|closed), labels, assignees; only the fields you pass are changed. Closing an issue = state: |
gitlab_add_issue_comment | write | Add a comment (note) to a GitLab issue. Returns { id, created_at }. Use gitlab_get_issue to read the issue first. |
gitlab_add_mr_comment | write | Comment on a merge request. Plain |
gitlab_approve_mr | write | Approve a merge request as the connected user. Non-destructive and revocable. Review the changes with gitlab_get_mr_diff first. Returns { approved: true, approvals_left }. (Approvals are a GitLab Premium feature on some instances; on those this 404s.) |
gitlab_cancel_pipeline | read | Cancel a running GitLab pipeline by id (cancels its running/pending jobs). Returns the slim pipeline; on failure returns GitLab |
gitlab_close_mr | write | Close (reject) a merge request without merging. The author must reopen it to resurrect it. Prefer leaving review feedback via gitlab_add_mr_comment if you only want revisions. Returns { iid, state }. |
gitlab_create_issue | write | Create a GitLab issue with optional description and labels. Returns the created issue (note its |
gitlab_create_mr | write | Open a GitLab merge request from sourceBranch into targetBranch (default |
gitlab_create_or_update_file | write | Create or update a single file in a GitLab project with a commit message (content is plain text). Auto-detects create vs update by checking whether the file already exists on |
gitlab_get_clone_url | read | Mint a temporary authenticated HTTPS git URL for a project (https://oauth2:<token>@host/path.git), usable for clone/pull/push (the |
gitlab_get_file | read | Read a file |
gitlab_get_issue | read | Get one GitLab issue: { iid, title, description (truncated to 2000 chars), state, author, labels, assignees, user_notes_count, web_url }. Reply with gitlab_add_issue_comment. |
gitlab_get_mr | write | Get one merge request: iid, title, description (truncated to 2000 chars), state, merge_status, author, source/target branches, draft, upvotes/downvotes, user_notes_count, web_url. Check this (state, merge_status, approvals) before gitlab_merge_mr. For the code changes use gitlab_get_mr_diff. |
gitlab_get_mr_diff | write | Read a merge request |
gitlab_get_pipeline | read | Get a single GitLab CI pipeline by id as a slim object { id, status, ref, sha, source, web_url, created_at, updated_at }. Use to poll a pipeline you triggered (status: created | pending | running | success | failed | canceled). |
gitlab_get_project | read | Get one GitLab project |
gitlab_list_branches | write | List branches in a GitLab project as slim rows { name, default, protected, merged }. Use gitlab_list_commits to see history on a branch. Defaults: 20 per page. |
gitlab_list_commits | read | List commits in a GitLab project as slim rows { id (12 chars), title, author, date }, newest first. Pass |
gitlab_list_issues | read | List issues in a GitLab project as slim rows { iid, title, state, author, labels, assignees, user_notes_count, updated_at }. Defaults: state=opened, 20 per page. Use gitlab_get_issue for one issue |
gitlab_list_mr_comments | write | List comments (notes) on a merge request as slim rows { id, author, body, system, resolvable, created_at }. system notes are GitLab |
gitlab_list_mr_commits | write | List the commits in a merge request as slim rows { id (12 chars), title, author, date }. Cheaper than the diff for understanding an MR |
gitlab_list_mrs | write | List merge requests in a GitLab project as slim rows { iid, title, state, author, source_branch, target_branch, draft, updated_at }. state defaults to opened; pass merged/closed/all for history. Use gitlab_get_mr for full detail. Defaults: 20 per page. |
gitlab_list_pipelines | read | List CI pipelines for a GitLab project as slim rows { id, status, ref, sha (12 chars), source, updated_at, web_url }, newest first. Optional |
gitlab_list_projects | read | List the authenticated user |
gitlab_list_releases | read | List releases for a GitLab project as slim rows { tag_name, name, released_at, upcoming }, newest first. Defaults: 20 per page. |
gitlab_merge_mr | write | Merge a merge request into its target branch. IRREVERSIBLE: this lands the commits — check state and merge_status with gitlab_get_mr first, and only merge when explicitly asked. Optional: merge_commit_message, squash, remove_source_branch. Returns the merged MR |
gitlab_retry_pipeline | read | Retry the failed and canceled jobs in a GitLab pipeline by id. Returns the slim pipeline; on failure returns GitLab |
gitlab_search_code | read | Search code (blobs) across the GitLab instance as slim rows { path, project_id, ref, startline, data }. Scope to a project with |
gitlab_search_issues | read | Search issues across the GitLab instance (or one project if |
gitlab_search_projects | read | Search projects across the GitLab instance by name/path as slim rows { id, path_with_namespace, visibility, default_branch, description }. Default 10 results. Use the id or path_with_namespace as |
gitlab_trigger_pipeline | write | Trigger a new CI pipeline for a GitLab project on a branch or tag ( |
gitlab_update_issue | write | Update a GitLab issue — set any of title, description, labels, and close/reopen it. Pass state= |
google_calendar_create_event | write | Create an event on a calendar (default \ |
google_calendar_delete_event | destructive | Delete a calendar event permanently (attendees are notified per calendar settings). Returns { success: true } on success. To merely change time/details, use google_calendar_update_event instead. |
google_calendar_freebusy | read | Check when calendars are busy between timeMin and timeMax (ISO 8601) — use this BEFORE google_calendar_create_event to pick a free slot and avoid double-booking. calendarIds defaults to [\ |
google_calendar_get_event | read | Get one calendar event by id. Returns the slim detail shape: { id, summary, start, end, location, status, organizer, htmlLink, description (truncated to 1000 chars), attendees: [{ email, responseStatus }] }. Get event ids from google_calendar_list_events. |
google_calendar_list_calendars | read | List the user |
google_calendar_list_events | read | List events on a calendar (default \ |
google_calendar_update_event | write | Update fields of an existing event (PATCH — only the fields you pass change). Returns the updated event in the same slim shape as google_calendar_get_event. To remove an event entirely use google_calendar_delete_event. |
google_docs_batch_update | destructive | Apply batch updates to a Google Doc (insert text, delete content, format, etc.) |
google_docs_create | write | Create a new Google Doc |
google_docs_get | read | Get a Google Doc by ID (returns full document structure) |
google_docs_get_plaintext | read | Get a Google Doc as plain text by ID |
google_docs_search | read | Search for Google Docs in Drive |
google_drive_create_folder | write | Create a folder in Google Drive |
google_drive_download | read | Download a file from Google Drive |
google_drive_list | read | List files in Google Drive, optionally filtered by a raw Drive query string ( |
google_drive_permissions | write | Update permissions for a Google Drive file |
google_drive_trash | write | Move a file to trash in Google Drive |
google_drive_upload | write | Upload a file to Google Drive |
google_drive_upload_from_url | write | Fetch a file from a signed or pre-authenticated URL (e.g. S3, GCS, CDN) and upload it directly to Google Drive — no need to pass file content inline. MIME type is auto-detected from the response Content-Type header when not specified. Returns the created Drive file { id, name, mimeType }. |
google_gemini_generate | read | Generate content with Google Gemini |
google_gmail_draft | write | Create a draft (saved to the Drafts folder, NOT sent — use google_gmail_send to actually send). |
google_gmail_get | read | Read one message in full: decoded plain-text body plus from/to/subject/date and messageIdHeader |
google_gmail_labels | read | List all label IDs and names (system labels like INBOX/UNREAD/STARRED plus user labels). |
google_gmail_list | read | List messages with sender/subject/date/snippet per row — use this for scanning a mailbox; use |
google_gmail_modify | write | Change a message |
google_gmail_profile | read | Get the connected account |
google_gmail_send | write | Send an email, or reply within an existing conversation. To reply in-thread you must pass BOTH |
google_gmail_threads | read | List conversation threads as {id, snippet, historyId} rows plus nextPageToken — one row per |
google_sheets_append | read | Append rows to the end of a table/log in a Google Sheet — THIS is the tool for adding new rows (Sheets finds the table in |
google_sheets_create | write | Create a new (empty) Google Spreadsheet with the given title, optionally with named tabs ( |
google_sheets_read | read | Read cell values from a Google Sheet range (e.g. \ |
google_sheets_search | read | Find Google Sheets by name (Drive name-contains match). Returns up to pageSize (default 10) rows of { id, name, modifiedTime } plus nextPageToken — use the id with google_sheets_read/write/append. For full-text or non-spreadsheet search use google_drive_search. |
google_sheets_write | destructive | OVERWRITE cell values in an exact range of a Google Sheet (PUT semantics — existing cells in the range are replaced). To add new rows to the end of a log/table WITHOUT touching existing data, use google_sheets_append instead. Returns { updatedRange, updatedCells }. |
google_slides_batch_update | destructive | Apply batch updates to a Google Slides presentation (insert text, create/delete slides, etc.) |
google_slides_create | write | Create a new Google Slides presentation |
google_slides_create_from_markdown | write | Create a Google Slides presentation from Markdown text. Use |
google_slides_get | read | Get a Google Slides presentation by ID |
google_slides_search | read | Search for Google Slides presentations in Drive |
httpbin_get_cookies | read | Get current cookies from httpbin |
httpbin_get_headers | read | Get request headers (useful to verify Cookie header is injected) |
httpbin_set_cookie | write | Set a cookie on httpbin |
jira_add_comment | write | Add a comment to a Jira issue; body is plain text (wrapped in ADF automatically). Returns { id, created } of the new comment — use jira_get_comments to read the thread. |
jira_create_issue | write | Create a Jira issue and return its { id, key, self }. description is plain text (wrapped in ADF automatically); issueType defaults to \ |
jira_get_boards | read | List Jira agile boards as { boards: [{ id, name, type, projectKey }], total, isLast }; optionally filter by projectKey (maxResults defaults to 50). Requires the read:board-scope:jira-software scope — reconnect Jira if this returns 401. |
jira_get_comments | read | Read the comments on a Jira issue as slim rows { id, author, body (plain text extracted from ADF), created }, oldest first (maxResults defaults to 10). Use jira_add_comment to reply. |
jira_get_create_meta | write | Return the fields required (and allowed) when creating a Jira issue. Pass projectKey and optionally issueType to scope the result. Each field entry includes { fieldId, name, required, schema, allowedValues } — use the fieldId as the key and an allowedValues entry |
jira_get_issue | read | Get a Jira issue |
jira_get_transitions | read | List the status transitions currently available for an issue as [{ id, name, toStatus }] — transitions depend on the issue |
jira_list_projects | read | List the Jira projects you can see as slim rows { key, id, name, projectTypeKey } (maxResults defaults to 25); pass query to filter by name/key. The standard way to discover a projectKey for jira_create_issue or JQL. |
jira_project_types | read | List the Jira project types available on this site as slim rows { key, formattedKey, type } (icons stripped). Rarely needed — use jira_list_projects to discover actual projects and their keys. |
jira_search_issues | read | Search Jira issues with a JQL query; returns slim rows { key, id, summary, status, assignee, priority, updated } plus nextPageToken for paging (maxResults defaults to 10). Pass fields to request other fields (returned raw under each row |
jira_search_users | read | Search Jira users by name or email; returns slim rows { accountId, displayName, emailAddress, active } (maxResults defaults to 10). Use the accountId with jira_update_issue |
jira_transition_issue | write | Move a Jira issue to another status by applying a workflow transition; returns { success: true }. transitionId must come from jira_get_transitions for the same issue (ids vary per workflow and current status). |
jira_update_issue | write | Update a Jira issue |
linear__create_issue | write | Create a Linear issue |
list_integrations | read | List all available integrations and connection status |
list_jot_files | read | List the files inside a jot you own (path, bytes, updatedAt), so you can see the current structure before making a partial update with update_jot. Returns FORBIDDEN if another user owns it, NOT_FOUND if it doesn |
list_jots | read | List the jots you have deployed (name, access, url, updatedAt). Only your own jots are returned. |
newrelic_add_notification_channels_to_policy | write | Associate existing (legacy) notification channels with an alert policy so it sends notifications when incidents open (alertsNotificationChannelsAddToPolicy). Provide the policy ID and one or more channel IDs. |
newrelic_add_tags_to_entity | write | Add tags (key + values) to a New Relic entity for filtering and organization (taggingAddTagsToEntity). Identify the entity by its GUID. Note: for APM agents a restart may be required before new tags appear. Returns { errors } (empty on success). |
Trust audit
BLOCKgrade F · trust 34/100 Do not install this without reading the findings. The audit found something that could harm you or your machine.
| Layer | What it checks | Result |
|---|---|---|
| L0 | Provenance & inventory | WARN |
| L1 | Static analysis of the code | FAIL |
| L2 | Instruction surface (what it tells the agent) | WARN |
| L3 | Class-specific surface | WARN |
| L4 | Behavioural (sandbox) | SKIPPED |
What the source does
- Filesystem
- declared (11 observation(s))
- Network
- declared (10 observation(s))
- Shell
- declared (2 observation(s))
- Dependencies
- not all pinned
- Secrets in source
- found
Findings (25)
exec(sql: string): Promise<void>;
exec(sql: string): void;
if (a === 169 && b === 254) return true; // link-local (incl. cloud metadata 169.254.169.254)
auth.ts
console.log(token);
console.log(`api key: ${apiKey}`);TEST_POSTGRES_URL: postgres://postgres:postgres@localhost:5432/workbench_test
TEST_POSTGRES_URL=postgres://postgres:[email protected]:5432/postgres npm run test -w @a-workbench/server
DATABASE_URL=postgres://user:[email protected]:5432/workbench # PostgreSQL
export TARGET_DATABASE_URL=postgres://user:[email protected]:5432/workbench
"TEST_POSTGRES_URL=postgres://postgres:[email protected]:5432/postgres to cover the PostgreSQL path."
const SECRET = "tok-abc-secret-value";
const SECRET = "tok-abc-secret-value";
access_token: "xoxp-user-token",
accessToken: "xoxp-user-token",
access_token: "xoxe.xoxp-user-token",
expect(callArgs.accessToken).toBe("xoxe.xoxp-user-token");`<span style="color:#fff;font-size:120px;font-weight:800;letter-spacing:-.03em">workbench</span></div>`,
clevertap_get_uninstall_report, confluence_delete_page, delete_jot, files_delete, google_calendar_delete_event, google_docs_batch_update, google_sheets_write, google_slides_batch_update, slack_remove_
workspace-paths.test.ts
.nojekyll
const f = new Function("location", "document", "isSecureContext", "HTMLInputElement", "HTMLTextAreaElement", "Event", `return (${fn});`)(const f = new Function("location", "document", "isSecureContext", "HTMLInputElement", "HTMLTextAreaElement", "Event", `return (${fn});`)(const { storeToken } = await import("../../../server/src/auth/tokens");import { renderWithClient } from "../../test-utils";Gates applied: no_behavioural_pass.
7477d052587dfull audit observations/trust-audit/mcp-server/amarthaid__workbench.json · Report an issue / request a re-scanAudit history
Every audit this server has had. A grade with a past is a grade somebody is still checking.
| Date | Source | Verdict | Grade | Score | Change |
|---|---|---|---|---|---|
| 2026-10-13 | 7477d052587d | BLOCK | F | 34 | first audit |
Questions
What is the workbench MCP server?
One MCP endpoint. Every tool your agent needs. Self-hosted, per-user OAuth. 20+ SaaS integrations and invite you to add more.
What tools does workbench expose?
200 in total: 140 read-only, 87 that write, and 9 that can delete or overwrite (clevertap_get_uninstall_report, confluence_delete_page, delete_jot, files_delete, google_calendar_delete_event). Every one is listed on this page with its risk.
Is workbench safe to connect to an agent?
No — not without reading the findings first. The audit graded it F (34/100) and found 3 critical or high issues in the source. Each one is listed on this page with the file and line it is on. Separately from the audit: 9 of its tools can destroy data, so scope the token you give it to what you actually need.
What credentials does workbench need?
It reads CAPTURE_PROXY_PASSWORD, GITHUB_CLIENT_SECRET, GITLAB_CLIENT_SECRET, GOOGLE_GMAIL_CLIENT_SECRET, NEW_RELIC_API_KEY, PGPASSWORD, SESSION_SECRET and SLACK_CLIENT_SECRET from the environment. Give it a token scoped to the least it needs — an agent that can be talked into calling a tool can be talked into calling it with your credentials.
How does workbench run?
It speaks streamable-http, so it runs as a service you connect to over the network. It is published on npm as @a-workbench/site at 0.1.0.
How current is this page?
The grade is for one exact copy of the source (7477d052587d), read on 2026-10-13. The repository is watched and re-audited when it changes.