
Connects to Drone CI/CD instances via personal access token and exposes 47 operations across the full platform surface. You get repo management (enable, disable, sync), build control (list, restart, cancel, promote, rollback), cron job scheduling, secret storage, and approval workflows for gated deployments. Useful when you want to query build status, trigger deployments, or manage CI configuration without leaving your chat interface. Supports both stdio for local MCP clients and SSE over HTTP for remote access. Requires DRONE_SERVER and DRONE_TOKEN environment variables pointed at your Drone instance.
A Model Context Protocol (MCP) server for interacting with Drone CI/CD. This server provides tools and resources to query build information, repositories, and more from your Drone instance.
--enable-write-toolsdrone://builds/{owner}/{repo}/{build}go mod tidy
go build -o drone-mcp-server .
| Variable | Required | Description |
|---|---|---|
DRONE_SERVER | yes | Base URL of the Drone instance, e.g. https://drone.example.com |
DRONE_TOKEN | yes | Drone personal access token, sent as Authorization: Bearer <token> on every Drone API call |
MCP_AUTH_TOKEN | HTTP mode | Bearer token that clients must present. HTTP mode refuses to start without it |
MCP_AUTH_TOKEN_FILE | no | File containing the bearer token (Docker/Kubernetes secrets). Takes precedence over MCP_AUTH_TOKEN |
MCP_ENABLE_WRITE_TOOLS | no | true registers the write and destructive tools, equivalent to --enable-write-tools |
export DRONE_SERVER=https://drone.example.com
export DRONE_TOKEN=your_drone_token
export MCP_AUTH_TOKEN="$(openssl rand -base64 32)" # HTTP mode only
Grant the smallest Drone permission set that covers the tools you expose. The read-only tool set only needs read access to repositories and builds, while write tools such as delete_user, create_org_secret or chown_repo require administrator permissions.
This server is a privileged proxy: whatever it can reach, anyone who can talk to it can reach too. The defaults reflect that.
MCP_AUTH_TOKEN (or MCP_AUTH_TOKEN_FILE) the server refuses to start in HTTP mode. --insecure-allow-anonymous overrides this and logs a warning.readOnlyHint, destructiveHint and idempotentHint so clients can gate writes themselves.StreamableHTTPHandler, which rejects requests that arrive on a loopback address with a non-loopback Host header and rejects cross-site browser requests.--allowed-hosts and --allowed-origins add explicit Host and Origin allowlists on top of the transport defaults.429).X-Forwarded-For / X-Real-IP are ignored unless --trust-proxy is set, and control characters are stripped from logged values so request data cannot forge log lines.MCP_AUTH_TOKEN (32+ random bytes) and inject it as a platform secret rather than on the command line.--trust-proxy only when a proxy you control rewrites X-Forwarded-For / X-Real-IP; otherwise the logged client address is attacker controlled.Add the server to your MCP client configuration (e.g., Claude Desktop):
{
"mcpServers": {
"drone": {
"command": "/path/to/drone-mcp-server",
"env": {
"DRONE_SERVER": "https://drone.example.com",
"DRONE_TOKEN": "your_token"
}
}
}
}
You can run the server directly for testing:
# Stdio mode (default) - for local MCP clients
./drone-mcp-server
# Streamable HTTP mode - requires MCP_AUTH_TOKEN
export MCP_AUTH_TOKEN="$(openssl rand -base64 32)"
./drone-mcp-server --http --host localhost --port 8080
Stdio mode (default): Communicates via stdin/stdout using the MCP protocol. Suitable for local integration with MCP clients; no network exposure and no authentication required.
Streamable HTTP mode (--http): A single endpoint that accepts POST (client to server), GET (server to client stream) and DELETE (session teardown).
Upgrading from earlier releases: the old
--sseflag and the SSE transport are gone. Use--httpand a client that speaks Streamable HTTP.
# Local only, read-only tools
MCP_AUTH_TOKEN=... ./drone-mcp-server --http
# Behind a TLS terminating proxy, write tools enabled
MCP_AUTH_TOKEN=... ./drone-mcp-server --http --host 127.0.0.1 --port 8080 \
--enable-write-tools --allowed-hosts mcp.example.com
Clients connect to http://localhost:8080/ by default and must send the header:
Authorization: Bearer <MCP_AUTH_TOKEN>
Behind a reverse proxy: the transport rejects a public Host header on a loopback connection with 403 as part of its DNS rebinding protection. Either keep the loopback Host header (proxy_set_header Host 127.0.0.1:8080;), or hand Host validation to the allowlist with a single flag:
./drone-mcp-server --http --allowed-hosts mcp.example.com
Setting --allowed-hosts replaces the loopback heuristic, so you do not need --localhost-protection=false. If you pass --localhost-protection=true explicitly, both checks are applied and the public Host is rejected again.
| Flag | Default | Description |
|---|---|---|
--http | false | Use the Streamable HTTP transport |
--host / --port | localhost / 8080 | Listen address (HTTP mode only) |
--path | / | Path of the MCP endpoint (HTTP mode only) |
--enable-write-tools | false | Register the 27 write and destructive tools |
--insecure-allow-anonymous | false | Allow HTTP mode without MCP_AUTH_TOKEN |
--trust-proxy | false | Trust X-Forwarded-For / X-Real-IP from a reverse proxy |
--allowed-hosts | (empty) | Comma-separated Host allowlist; replaces the loopback heuristic |
--allowed-origins | (empty) | Comma-separated browser Origin allowlist |
--localhost-protection | true | Reject loopback connections carrying a non-loopback Host header; set explicitly together with --allowed-hosts to stack both checks |
--session-timeout | 30m | Idle MCP session timeout (HTTP mode only) |
--max-request-bytes | 1048576 | Maximum MCP request body size |
--version | Print version information and exit |
The server implements 45 tools. In the default read-only mode only the 18 read-only tools are registered; the remaining 27 are write or destructive tools and require --enable-write-tools (or MCP_ENABLE_WRITE_TOOLS=true).
Write and destructive tools: enable_repo, disable_repo, repair_repo, chown_repo, sync_repos, create_cron, delete_cron, execute_cron, create_secret, update_secret, delete_secret, create_org_secret, update_org_secret, delete_org_secret, create_user, update_user, delete_user, create_template, update_template, delete_template, restart_build, cancel_build, promote_build, rollback_build, approve_build, decline_build, create_build.
The destructive ones (deletions, disables, cancellations, promotions, rollbacks, ownership changes) are advertised with destructiveHint: true; the rest of the write tools with destructiveHint: false. All 18 read-only tools are advertised with readOnlyHint: true.
list_reposLists all repositories in your Drone instance.
get_repoGet repository details.
Arguments:
owner (string): Repository ownerrepo (string): Repository nameenable_repoEnable a repository.
Arguments:
owner (string): Repository ownerrepo (string): Repository namedisable_repoDisable a repository.
Arguments:
owner (string): Repository ownerrepo (string): Repository namerepair_repoRepair a repository.
Arguments:
owner (string): Repository ownerrepo (string): Repository namechown_repoChange repository ownership.
Arguments:
owner (string): Repository ownerrepo (string): Repository namesync_reposSynchronize repository list.
list_incompleteList repositories with incomplete builds.
list_buildsLists builds for a specific repository.
Arguments:
owner (string): Repository ownerrepo (string): Repository nameget_buildGet detailed information about a specific build.
Arguments:
owner (string): Repository ownerrepo (string): Repository namebuild (number): Build numberget_build_lastGet the last build for a repository (optionally by branch).
Arguments:
owner (string): Repository ownerrepo (string): Repository namebranch (string, optional): Branch nameget_build_logsGet logs for a specific build stage and step.
Arguments:
owner (string): Repository ownerrepo (string): Repository namebuild (number): Build numberstage (number): Stage numberstep (number): Step numberrestart_buildRestart a build (optionally with parameters).
Arguments:
owner (string): Repository ownerrepo (string): Repository namebuild (number): Build numberparams (object, optional): Build parameterscancel_buildCancel a running build.
Arguments:
owner (string): Repository ownerrepo (string): Repository namebuild (number): Build numberpromote_buildPromote a build to a target environment.
Arguments:
owner (string): Repository ownerrepo (string): Repository namebuild (number): Build numbertarget (string): Target environmentparams (object, optional): Promotion parametersrollback_buildRollback a deployment to a previous build.
Arguments:
owner (string): Repository ownerrepo (string): Repository namebuild (number): Build numbertarget (string): Target environmentparams (object, optional): Rollback parametersapprove_buildApprove a build stage (for gated deployments).
Arguments:
owner (string): Repository ownerrepo (string): Repository namebuild (number): Build numberstage (number): Stage numberdecline_buildDecline a build stage (for gated deployments).
Arguments:
owner (string): Repository ownerrepo (string): Repository namebuild (number): Build numberstage (number): Stage numbercreate_buildCreate a new build from a commit or branch.
Arguments:
owner (string): Repository ownerrepo (string): Repository namecommit (string, optional): Commit SHAbranch (string, optional): Branch nameparams (object, optional): Build parameterslist_cronsList cron jobs for a repository.
Arguments:
owner (string): Repository ownerrepo (string): Repository nameget_cronGet cron job details.
Arguments:
owner (string): Repository ownerrepo (string): Repository namecron (string): Cron job namecreate_cronCreate a new cron job.
Arguments:
owner (string): Repository ownerrepo (string): Repository namename (string): Cron job nameexpr (string): Cron expressionbranch (string): Branch namedisable (boolean, optional): Disable the cron jobdelete_cronDelete a cron job.
Arguments:
owner (string): Repository ownerrepo (string): Repository namecron (string): Cron job nameexecute_cronExecute a cron job immediately.
Arguments:
owner (string): Repository ownerrepo (string): Repository namecron (string): Cron job namelist_secretsList repository secrets.
Arguments:
owner (string): Repository ownerrepo (string): Repository nameget_secretGet repository secret details.
Arguments:
owner (string): Repository ownerrepo (string): Repository namename (string): Secret namecreate_secretCreate a repository secret.
Arguments:
owner (string): Repository ownerrepo (string): Repository namename (string): Secret namevalue (string): Secret valuepull_request (boolean, optional): Allow in pull requestspull_request_push (boolean, optional): Allow in pull request push eventsupdate_secretUpdate a repository secret.
Arguments:
owner (string): Repository ownerrepo (string): Repository namename (string): Secret namevalue (string): Secret valuepull_request (boolean, optional): Allow in pull requestspull_request_push (boolean, optional): Allow in pull request push eventsdelete_secretDelete a repository secret.
Arguments:
owner (string): Repository ownerrepo (string): Repository namename (string): Secret namelist_org_secretsList organization secrets.
Arguments:
namespace (string): Organization namespaceget_org_secretGet organization secret details.
Arguments:
namespace (string): Organization namespacename (string): Secret namecreate_org_secretCreate an organization secret.
Arguments:
namespace (string): Organization namespacename (string): Secret namevalue (string): Secret valuepull_request (boolean, optional): Allow in pull requestspull_request_push (boolean, optional): Allow in pull request push eventsupdate_org_secretUpdate an organization secret.
Arguments:
namespace (string): Organization namespacename (string): Secret namevalue (string): Secret valuepull_request (boolean, optional): Allow in pull requestspull_request_push (boolean, optional): Allow in pull request push eventsdelete_org_secretDelete an organization secret.
Arguments:
namespace (string): Organization namespacename (string): Secret nameget_selfGet current authenticated user.
list_usersList all users.
get_userGet user details.
Arguments:
login (string): User login namecreate_userCreate a new user.
Arguments:
login (string): User login nameemail (string, optional): User emailadmin (boolean, optional): Admin privilegesactive (boolean, optional): Active statustoken (string, optional): User tokenupdate_userUpdate a user.
Arguments:
login (string): User login nameadmin (boolean, optional): Admin privilegesactive (boolean, optional): Active statusdelete_userDelete a user.
Arguments:
login (string): User login namelist_templatesList templates (optionally by namespace).
Arguments:
namespace (string, optional): Template namespaceget_templateGet template details and data.
Arguments:
namespace (string): Template namespacename (string): Template namecreate_templateCreate a new template.
Arguments:
namespace (string): Template namespacename (string): Template namedata (string): Template data (YAML)update_templateUpdate a template.
Arguments:
namespace (string): Template namespacename (string): Template namedata (string): Template data (YAML)delete_templateDelete a template.
Arguments:
namespace (string): Template namespacename (string): Template nameAccess build details via resource URI: drone://builds/{owner}/{repo}/{build}
Example:
Read resource: drone://builds/owner1/repo1/123
A multi-architecture Docker image is available on GitHub Container Registry:
# Pull the latest image
docker pull ghcr.io/yusiwen/drone-mcp-server:latest
# Run in stdio mode (for local MCP clients)
docker run --rm -i \
-e DRONE_SERVER=https://drone.example.com \
-e DRONE_TOKEN=your_token \
ghcr.io/yusiwen/drone-mcp-server
# Run in Streamable HTTP mode: read-only, authenticated, published on loopback
docker run --rm \
-e DRONE_SERVER=https://drone.example.com \
-e DRONE_TOKEN=your_token \
-e MCP_AUTH_TOKEN=your_mcp_token \
-p 127.0.0.1:8080:8080 \
ghcr.io/yusiwen/drone-mcp-server --http --host 0.0.0.0
# Add --enable-write-tools when the workflow must mutate Drone
docker run --rm \
-e DRONE_SERVER=https://drone.example.com \
-e DRONE_TOKEN=your_token \
-e MCP_AUTH_TOKEN=your_mcp_token \
-p 127.0.0.1:8080:8080 \
ghcr.io/yusiwen/drone-mcp-server --http --host 0.0.0.0 --enable-write-tools
--host 0.0.0.0 is required inside a container so that published ports work, which is exactly why the bearer token is mandatory. Publish the port on 127.0.0.1 unless a TLS terminating proxy sits in front.
Prefer a mounted secret over an environment variable:
docker run --rm \
-v /run/secrets/mcp_token:/run/secrets/mcp_token:ro \
-e MCP_AUTH_TOKEN_FILE=/run/secrets/mcp_token \
-e DRONE_SERVER=https://drone.example.com \
-e DRONE_TOKEN=your_token \
-p 127.0.0.1:8080:8080 \
ghcr.io/yusiwen/drone-mcp-server --http --host 0.0.0.0
services:
drone-mcp-server:
image: ghcr.io/yusiwen/drone-mcp-server:latest
environment:
DRONE_SERVER: https://drone.example.com
DRONE_TOKEN: ${DRONE_TOKEN:?DRONE_TOKEN is required}
MCP_AUTH_TOKEN: ${MCP_AUTH_TOKEN:?MCP_AUTH_TOKEN is required}
# MCP_ENABLE_WRITE_TOOLS: "true" # only when the workflow needs writes
ports:
- "127.0.0.1:8080:8080"
command: ["--http", "--host", "0.0.0.0"]
restart: unless-stopped
read_only: true
security_opt:
- no-new-privileges:true
Pre-built binaries are available for Linux (x64, arm64), macOS (x64, arm64), and Windows (x64) in the GitHub Releases.
The binary includes version information:
./drone-mcp-server --version
Output example:
drone-mcp-server
Version: v1.0.0
Commit: abc123
Build date: 2024-01-01T00:00:00Z
Go version: go1.25.1
.
├── main.go # Entry point: CLI flags, transports, auth, HTTP hardening, tool registration
├── main_test.go # Auth middleware, allowlists, log hygiene and tool registration tests
├── tool/ # Tool handlers module
│ ├── build.go # Build-related tools (list_builds, get_build, restart_build, etc.)
│ ├── repo.go # Repository-related tools (list_repos, enable_repo, disable_repo, etc.)
│ ├── resource.go # Resource handling (drone://builds/...)
│ ├── cron.go # Cron job management tools
│ ├── secret.go # Secret management tools
│ ├── user.go # User management tools
│ ├── template.go # Template management tools
│ ├── validate.go # Input validation helpers used by every tool
│ ├── validate_args.go # Validate implementation for each tool argument struct
│ └── validate_test.go # Validation tests, including injection attempts
├── SECURITY.md # Threat model, deployment guidance and reporting process
├── server.json # MCP registry metadata
├── test_env.sh # Manual smoke test script (uses environment variables)
├── test_mcp.go # In-memory MCP integration test (build tag: test)
└── README.md
go.mod; earlier Go 1.25 patch releases carry standard library vulnerabilities that fail the govulncheck gate)make build # build with version info injected through ldflags
go build -o drone-mcp-server .
make test # unit tests
make test-race # with the race detector
make smoke # end-to-end security smoke test
make vuln # govulncheck (module + standard library)
go vet ./...
The unit tests cover the authentication middleware (including constant time comparison and failure throttling), the Host policy, log sanitization, client address handling, input validation and the read-only tool registration. They need no Drone instance.
scripts/security-smoke-test.sh starts the real binary on a loopback port and asserts the behaviour that matters, without contacting any Drone instance:
./scripts/security-smoke-test.sh # builds the binary first
BIN=./drone-mcp-server ./scripts/security-smoke-test.sh
It checks that HTTP mode refuses to start without a token, that missing/wrong credentials, wrong Content-Type, oversized bodies, forged Host headers and cross-site Origin are rejected with 401/401/415/413/403/403, that the default tool list is exactly 18 read-only tools and that hidden write tools are not callable, that path traversal in tool arguments is refused, that log injection and X-Forwarded-For spoofing are neutralised, that --allowed-hosts replaces the loopback heuristic, that write tools appear only with --enable-write-tools, that --sse is gone, and that SIGTERM shuts the server down. It exits non-zero on the first failure set and runs in CI, so it doubles as a regression gate.
Use a read-only token first and confirm the tools answer:
export DRONE_SERVER=https://ci.example.com
export DRONE_TOKEN=<read-only token>
export MCP_AUTH_TOKEN="$(openssl rand -base64 32)"
./drone-mcp-server --http --host 127.0.0.1 --port 8080
Then point an MCP client at it. Stdio mode:
{
"mcpServers": {
"drone": {
"command": "/path/to/drone-mcp-server",
"env": { "DRONE_SERVER": "https://ci.example.com", "DRONE_TOKEN": "..." }
}
}
}
Streamable HTTP mode (most clients accept a headers block; npx @modelcontextprotocol/inspector works too, pick "Streamable HTTP" and add the same header):
{
"mcpServers": {
"drone": {
"url": "http://127.0.0.1:8080/",
"headers": { "Authorization": "Bearer <MCP_AUTH_TOKEN>" }
}
}
}
Suggested checks against a real instance: get_self (confirms the token works), list_repos, get_build_last, get_build_logs (exercises untrusted content in the model context), then get_repo with owner: "../users" to confirm validation refuses it. Run write tools against a disposable repository only.
export DRONE_SERVER=https://drone.example.com
export DRONE_TOKEN=...
# Read-only stdio server
./drone-mcp-server
# HTTP mode with authentication and write tools
MCP_AUTH_TOKEN=dev-token ./drone-mcp-server --http --enable-write-tools
MIT