Gridctl
π§ͺ Local Stack for testing Agents
Ask AI about Gridctl
Powered by Claude Β· Grounded in docs
I know everything about Gridctl. Ask me about installation, configuration, usage, or troubleshooting.
0/500
Reviews
Documentation
One endpoint. Dozens of AI tools. Zero configuration drift.

Gridctl aggregates tools from multiple MCP servers into a single gateway. Connect Claude Desktop - or any MCP client - to your grid through one endpoint and start building.
Define your stack in YAML. Apply with one command. Done.
gridctl apply stack.yaml
[!NOTE] Inspiration - This project was heavily influenced by Containerlab, a project I've used heavily over the years to rapidly prototype repeatable environments for the purpose of validation, learning, and teaching. Just like Containerlab, Gridctl is designed for fast, ephemeral, stateless, and disposable environments.
β‘οΈ Why Gridctl
MCP servers are everywhere. Running them shouldn't require a PhD in container orchestration. Or, is the MCP server not running in a container? Is a single endpoint exposed behind an existing platform? Is another team hosting and managing an MCP server that is on a different machine on the same network? Different transport types, methods of hosting, and .json files start to accumulate like dust.
I originally built this project to have a way to leverage a single configuration in my application, that I never have to update, while still building various combinations of MCP servers for rapid prototyping and learning.
I would rather be building than juggling ports, tracking environment variables, and hoping everything with my setup is ready for the next demo. My client now connects once and accesses everything over localhost:8180/sse by default.
version: "1"
name: stack
mcp-servers:
# Build GitHub MCP locally (instantiate in Docker container)
- name: github
image: ghcr.io/github/github-mcp-server:latest
transport: stdio
tools: ["get_file_contents", "search_code", "list_commits", "get_pull_request"]
env:
GITHUB_PERSONAL_ACCESS_TOKEN: "${GITHUB_PERSONAL_ACCESS_TOKEN}"
# Connects to external SaaS/Cloud Atlassian Rovo MCP Server (breaks out into OAuth to connect)
- name: atlassian
command: ["npx", "mcp-remote", "https://mcp.atlassian.com/v1/sse"]
# Turn any REST API into MCP tools via OpenAPI spec
- name: my-api
openapi:
spec: https://api.example.com/openapi.json
baseUrl: https://api.example.com
Three servers. Three different transports. One endpoint. Navigate to localhost:8180 to visualize the stack π

πͺ Installation
Quick install (macOS, Linux, WSL2)
curl -fsSL https://raw.githubusercontent.com/gridctl/gridctl/main/install.sh | sh
Installs the latest release to ~/.local/bin/gridctl. The script verifies the
release checksum and prints the install path and next steps.
The script can be inspected before running:
curl -fsSL https://raw.githubusercontent.com/gridctl/gridctl/main/install.sh | less
Windows: install WSL2, then run the command above inside your Linux distribution.

Package managers
Homebrew (macOS, Linux)
brew install gridctl/tap/gridctl
Update with brew upgrade gridctl/tap/gridctl.
Other options
Pre-built binaries
Download the tarball for your platform from the releases page,
verify it against checksums.txt, extract, and place gridctl on your PATH.
Build from source
Requires Go 1.26+ and Node 20+.
git clone https://github.com/gridctl/gridctl
cd gridctl && make build
./gridctl --help
Updating
gridctl upgrade # check + prompt + upgrade (standalone install)
gridctl upgrade --check # only check; do not install
gridctl upgrade --yes # non-interactive (CI)
gridctl upgrade --version v0.1.0-beta.6 # install a specific version
If gridctl was installed via Homebrew, gridctl upgrade detects that and recommends brew upgrade gridctl/tap/gridctl instead.
Uninstalling
# Standalone install
curl -fsSL https://raw.githubusercontent.com/gridctl/gridctl/main/install.sh | sh -s -- --uninstall
# Also remove the config directory at ~/.gridctl
curl -fsSL https://raw.githubusercontent.com/gridctl/gridctl/main/install.sh | sh -s -- --uninstall --purge
# Homebrew install
brew uninstall gridctl/tap/gridctl
π Container Runtime
Gridctl requires a container runtime for workloads that run in containers (MCP servers with image and resources). Docker is detected by default; Podman is also fully supported.
Runtime Detection
Gridctl auto-detects your runtime by probing sockets in this order:
$DOCKER_HOST(if set)/var/run/docker.sock(Docker)/run/podman/podman.sock(Podman rootful)$XDG_RUNTIME_DIR/podman/podman.sock(Podman rootless)
Override detection with the --runtime flag or GRIDCTL_RUNTIME environment variable:
gridctl apply stack.yaml --runtime podman
# or
GRIDCTL_RUNTIME=podman gridctl apply stack.yaml
Using Podman
# Install Podman (macOS)
brew install podman
podman machine init
podman machine start
# Install Podman (Linux)
sudo apt install podman # Debian/Ubuntu
sudo dnf install podman # Fedora/RHEL
# Enable the Podman socket (Linux rootless)
systemctl --user enable --now podman.socket
# Verify gridctl detects Podman
gridctl info
Podman 4.0+ is required for rootless multi-container networking (netavark + aardvark-dns). Podman 4.7+ is recommended for full host.containers.internal support. Older versions fall back to the Docker-compatible host.docker.internal alias. SELinux volume labels (:Z) are applied automatically when Podman is running on an SELinux-enforcing system.
π¦ Quick Start
# Apply the example stack
gridctl apply examples/getting-started/skills-basic.yaml
# Check what's running
gridctl status
# Open the web UI
open http://localhost:8180
# Clean up
gridctl destroy examples/getting-started/skills-basic.yaml
π¬ Features
Stack as Code
Fast, consistent, ephemeral, flexible, and version controlled! Many practitioners use different combinations of MCP servers depending on what they are working on. Being able to instantiate, from a single file, the various combinations needed for the right task, saves time in development and prototyping. The stack.yaml file is where you define this.
Spec-Driven Workflow
The stack.yaml file has always been your source of truth. Now you have the full lifecycle tooling to match β validate before you commit, preview before you apply, and detect the moment your environment drifts from what's in version control:
gridctl validate stack.yaml # Lint and schema-check the spec (exit 0/1/2)
gridctl plan stack.yaml # Diff against running state β see exactly what changes
gridctl apply stack.yaml # Apply the spec
gridctl export # Reverse-engineer stack.yaml from a running stack
gridctl test <skill> # Run acceptance criteria for a skill (exit 0/1/2)
gridctl activate <skill> # Promote a skill from draft to active
Drift detection runs in the background: the canvas flags servers that are running but absent from your spec, and declarations in your spec that haven't been deployed β so your YAML and your environment stay in sync. Need to build a stack from scratch? Start the UI with gridctl serve, use the visual spec builder to compose your stack through a guided wizard, then Save & Load it directly into the running daemon β no YAML file required to get started.
Executable skills (those with a workflow block) must define acceptance_criteria before gridctl activate will promote them β ensuring every deployed skill has a machine-checkable definition of done.
Protocol Bridge
Aggregates tools from HTTP servers, stdio processes, SSH tunnels, and external URLs into a unified gateway. Automatic namespacing (server__tool) prevents collisions.
Transport Flexibility
| Transport | Config | When to Use |
|---|---|---|
| Container HTTP | image + port | Dockerized MCP servers |
| Container Stdio | image + transport: stdio | Servers using stdin/stdout |
| Local Process | command | Host-native MCP servers |
| SSH Tunnel | command + ssh.host | Remote machine access |
| External URL | url | Existing infrastructure |
| OpenAPI Spec | openapi.spec | Any REST API with an OpenAPI spec |
Context Window Optimization (access control)
Are you paying for your own tokens for learning? Even if you aren't, being optimized is critical for not overloading that context window! Reducing the number of tools and scoping things correctly significantly reduces the likelihood of "tool confusion" β where a given LLM selects a similarly named tool from the wrong server.
Use the tools filter in the stack.yaml file to whitelist exactly which tools each server exposes. gridctl filters this list before it reaches the LLM:
mcp-servers:
- name: github
image: ghcr.io/github/github-mcp-server:latest
transport: stdio
tools: ["get_file_contents", "search_code", "list_commits", "get_issue", "get_pull_request"]
env:
GITHUB_PERSONAL_ACCESS_TOKEN: "${GITHUB_PERSONAL_ACCESS_TOKEN}"
This GitHub server only exposes read-only tools. Write operations like create_issue and create_pull_request are hidden from all clients.
Output Format Conversion
Tool call results default to JSON. Set output_format at the gateway or per-server level to convert structured responses into TOON or CSV before they reach the client β reducing token consumption by 25β61% for tabular and key-value data.
gateway:
output_format: toon # Default for all servers: json, toon, csv, text
mcp-servers:
- name: analytics
image: my-org/analytics:latest
port: 8080
output_format: csv # Override: this server returns CSV
| Format | Best For | Savings |
|---|---|---|
toon | Key-value pairs, nested objects | ~25β40% |
csv | Tabular / array-of-objects data | ~40β61% |
text | Raw passthrough (no conversion) | β |
json | Default (no conversion) | β |
Non-JSON responses and payloads over 1MB are passed through unchanged. Per-server settings override the gateway default.
Code Mode
When a stack exposes dozens of tools, context window consumption grows fast. Code Mode replaces all individual tool definitions with two meta-tools β search and execute β reducing context overhead by 99%+. LLM agents discover tools via search, then call them through JavaScript executed in a sandboxed goja runtime.
gateway:
code_mode: "on"
code_mode_timeout: 30 # Execution timeout in seconds (default: 30)
Or enable via CLI flag:
gridctl apply stack.yaml --code-mode
The sandbox provides mcp.callTool(serverName, toolName, args) for synchronous tool calls and console.log/warn/error for output capture. Modern JavaScript syntax (arrow functions, destructuring, template literals) is supported via esbuild transpilation. See examples/code-mode/ for a working example.
Skills Registry
Store reusable skills as SKILL.md files β markdown documents with YAML frontmatter that get exposed to LLM clients as MCP prompts. Create them via the REST API, Web UI, or by dropping files into ~/.gridctl/registry/skills/.
~/.gridctl/registry/skills/
βββ code-review/
βββ SKILL.md # Frontmatter + markdown instructions
βββ references/ # Optional supporting files
Skills have three lifecycle states: draft (stored, not exposed), active (discoverable via MCP), and disabled (hidden without deletion). See examples/registry/ for working examples.
Skill Workflows
Add inputs, workflow, and output blocks to a SKILL.md frontmatter to make it executable. Executable skills are exposed as MCP tools and run deterministic multi-step tool orchestration through the gateway.
inputs:
a: { type: number, required: true }
b: { type: number, required: true }
workflow:
- id: add
tool: math__add
args: { a: "{{ inputs.a }}", b: "{{ inputs.b }}" }
- id: echo
tool: text__echo
args: { message: "{{ steps.add.result }}" }
depends_on: add
output:
format: last
Steps without dependencies run in parallel. Template expressions reference inputs ({{ inputs.x }}) and prior step results ({{ steps.id.result }}). Each step supports retry policies, timeouts, conditional execution, and configurable error handling (fail / skip / continue). The Web UI includes a visual workflow designer with Code, Visual, and Test modes. See examples/registry/ for working examples.
Private Repositories
Both gridctl skill add and MCP server source blocks can clone private git repositories. Credentials come from one of three places, in priority order:
- Vault reference (recommended) β the raw token stays in the encrypted vault; only a
${vault:KEY}reference is persisted to the skill origin / lock file. - Ephemeral flag β
--auth-token <PAT>for one-shot CI use; never written to disk. - Ambient environment β SSH URLs use ssh-agent +
~/.ssh/known_hosts; HTTPS URLs fall back toGITHUB_TOKENif set.
# Public repo (unchanged)
gridctl skill add https://github.com/acme/public-skills
# Private HTTPS with a vault-stored PAT (re-resolved on every update)
gridctl vault set GIT_TOKEN ghp_xxxxxxxxxxxxxxxxxxxx
gridctl skill add https://github.com/acme/private-skills --vault-key GIT_TOKEN
# Private HTTPS with an ephemeral PAT (CI use; not persisted)
gridctl skill add https://github.com/acme/private-skills --auth-token "$GIT_TOKEN"
# Private SSH via ambient ssh-agent (no flags)
gridctl skill add git@github.com:acme/private-skills.git
--auth-token and --vault-key are mutually exclusive. Both flags also work on gridctl skill try. gridctl skill update automatically re-resolves any stored ${vault:KEY} reference.
In the web wizard, the "Add skill source" step has an inline, collapsible Authentication card. It stays collapsed for public repos and auto-expands when a scan returns an auth-class error, offering two modes: pick an existing vault secret or paste a one-shot token. The same subsection is available in the MCP server form when the source type is git.
Raw tokens are never written outside the encrypted vault β neither to the skill origin nor the lock file. Error and log paths strip embedded URL userinfo (https://TOKEN@host/...) and known PAT patterns (ghp_β¦, github_pat_β¦, glpat-β¦) before they reach the API or CLI.
Cost Optimize
gridctl optimize scans the running gateway and prints findings with a measured weekly USD impact and a paste-ready YAML remediation. The PR-4 heuristics flag unused servers (registered but no calls observed in the lookback window) and unused tools (a server is active but a specific tool has not been called in the window and is not already excluded). On a fresh gateway with less than 24h of data, optimize returns a single info finding so reports never over-fire.
gridctl optimize # styled findings table
gridctl optimize --format json # machine-readable OptimizeReport
gridctl optimize --min-impact 0.10 # filter low-impact findings (info findings always shown)
gridctl optimize --severity warn,critical # narrow to actionable findings
Exit codes follow the standard CLI contract: 0 no findings or info-only, 1 at least one warn/critical finding, 2 infrastructure error (gateway unreachable, wrong stack name). The Web UI surfaces the same findings inside the Gateway sidebar's Optimize panel.
Distributed Tracing
Every tool call through the gateway is captured as an OpenTelemetry trace. Spans record transport type, server name, duration, and error state. The last 1000 traces are kept in a ring buffer and are queryable via CLI or the Web UI.
# List recent traces
gridctl traces
# Inspect a single trace as a span waterfall
gridctl traces <trace-id>
# Stream traces in real time
gridctl traces --follow
The Web UI includes a Traces tab in the bottom panel with an interactive waterfall view, span detail panel, and a pop-out window. Canvas edges light up with latency heat based on recent trace data.
π CLI Reference
gridctl validate <stack.yaml> # Validate stack YAML (exit 0/1/2)
gridctl validate <stack.yaml> --format json # Machine-readable output
gridctl plan <stack.yaml> # Preview changes against running state
gridctl plan <stack.yaml> -y # Auto-approve and apply planned changes
gridctl apply <stack.yaml> # Start containers and gateway
gridctl apply <stack.yaml> -f # Run in foreground (debug mode)
gridctl apply <stack.yaml> -p 9000 # Custom gateway port
gridctl apply <stack.yaml> --base-port 9000 # Base port for MCP server host port allocation
gridctl apply <stack.yaml> --watch # Watch for changes and hot reload
gridctl apply <stack.yaml> --flash # Apply and auto-link LLM clients
gridctl apply <stack.yaml> --code-mode # Enable code mode (search + execute)
gridctl apply <stack.yaml> --no-cache # Force rebuild of source-based images
gridctl apply <stack.yaml> --no-expand # Disable env var expansion in OpenAPI specs
gridctl apply <stack.yaml> -v # Print full stack as JSON
gridctl apply <stack.yaml> -q # Suppress progress output
gridctl apply <stack.yaml> --log-file <path> # Structured JSON log output with rotation
gridctl export # Reverse-engineer stack.yaml from running stack
gridctl export -o ./output # Write to directory instead of stdout
gridctl export --format json # Output as JSON instead of YAML
gridctl serve # Start the web UI without managing a stack
gridctl stop # Stop the stackless gridctl daemon
gridctl status # Show running stacks
gridctl status --replicas # Expand to one row per replica
gridctl info # Show detected container runtime
gridctl version # Print version information
gridctl upgrade # Check + prompt + upgrade (standalone install)
gridctl upgrade --check # Only check for updates; do not install
gridctl upgrade --yes # Non-interactive upgrade (CI)
gridctl upgrade --version <tag> # Install a specific release tag (allows downgrades)
gridctl upgrade --force # Bypass Homebrew detection and up-to-date short-circuit
gridctl link # Connect an LLM client to the gateway
gridctl unlink # Remove gridctl from an LLM client
gridctl reload # Hot reload a running stack
gridctl destroy <stack.yaml> # Stop and remove containers
gridctl vault set <key> # Store a secret (interactive prompt, or use --value)
gridctl vault get <key> # Retrieve a secret (masked by default, use --plain)
gridctl vault list # List all vault keys
gridctl vault delete <key> # Remove a secret from the vault
gridctl vault import <file> # Import secrets from .env or .json
gridctl vault export # Export secrets (default: env format)
gridctl vault lock / unlock # Lock or unlock the vault
gridctl vault change-passphrase # Change the vault encryption passphrase
gridctl skill list # List skills in the registry
gridctl skill add <repo-url> # Import skills from a remote git repository
gridctl skill add <repo-url> --auth-token <pat> # ...with ephemeral HTTPS PAT (CI; not persisted)
gridctl skill add <repo-url> --vault-key <key> # ...with a PAT resolved from ${vault:KEY}
gridctl skill add <repo-url> --ssh-key <path> # ...with an on-disk SSH private key
gridctl skill update [name] # Update imported skills (all if no name given)
gridctl skill remove <name> # Remove an imported skill
gridctl skill pin <name> <ref> # Pin a skill to a specific git ref
gridctl skill info <name> # Show skill origin and update status
gridctl skill try <repo-url> # Temporarily import a skill for evaluation
gridctl skill validate <name> # Validate a skill definition
gridctl test <skill-name> # Run acceptance criteria for a skill (exit 0/1/2)
gridctl activate <skill-name> # Promote a skill from draft to active state
gridctl traces # Show recent distributed traces (table view)
gridctl traces <trace-id> # Show span waterfall for a single trace
gridctl traces --follow # Stream new traces as they arrive
gridctl traces --server <name> # Filter by MCP server name
gridctl traces --errors # Show only error traces
gridctl traces --min-duration 100ms # Filter by minimum duration
gridctl traces --json # Output as JSON
gridctl optimize # Surface unused servers and tools with weekly $ impact
gridctl optimize --stack <name> # Pick a specific stack when more than one is running
gridctl optimize --min-impact 0.10 # Filter findings below a weekly USD impact threshold
gridctl optimize --severity warn,critical # Allowlist by severity
gridctl optimize --format json # Machine-readable OptimizeReport (exit 0/1/2)
π₯οΈ Connect LLM Application
The easiest way to connect is with gridctl link, which auto-detects installed LLM clients and injects the gateway configuration:
gridctl link # Interactive: detect and select clients
gridctl link claude # Link a specific client
gridctl link --all # Link all detected clients at once
Supported clients: Claude Desktop, Claude Code, Cursor, Windsurf, VS Code, Gemini, OpenCode, Continue, Cline, AnythingLLM, Roo, Zed, Goose
Manual configuration
Most Applications
{
"mcpServers": {
"gridctl": {
"url": "http://localhost:8180/sse"
}
}
}
Claude Desktop
{
"mcpServers": {
"gridctl": {
"command": "npx",
"args": ["-y", "mcp-remote", "http://localhost:8180/sse", "--allow-http", "--transport", "sse-only"]
}
}
}
Restart Claude Desktop after editing. All tools from your stack are now available.
π Examples
| Example | What It Shows |
|---|---|
mcp-basic.yaml | Stack with multiple MCP servers and tool filtering |
tool-filtering.yaml | Server-level tool access control |
local-mcp.yaml | Local process transport |
ssh-mcp.yaml | SSH tunnel transport |
external-mcp.yaml | External HTTP/SSE servers |
gateway-basic.yaml | Gateway to an existing MCP server |
gateway-remote.yaml | Remote access to Gridctl from other machines |
github-mcp.yaml | GitHub MCP server integration |
atlassian-mcp.yaml | Atlassian Rovo (Jira, Confluence) integration |
zapier-mcp.yaml | Zapier automation platform integration |
chrome-devtools-mcp.yaml | Chrome DevTools browser automation |
context7-mcp.yaml | Up-to-date library documentation |
openapi-basic.yaml | Turn a REST API into MCP tools via OpenAPI spec |
openapi-auth.yaml | OpenAPI with bearer token and API key auth |
code-mode-basic.yaml | Gateway code mode with search + execute meta-tools |
registry-basic.yaml | Skills registry with a single server |
registry-advanced.yaml | Cross-server skills |
workflow-basic | Executable skill workflow with sequential steps |
workflow-parallel | Fan-out parallel execution with fan-in merge |
workflow-conditional | Retry policies and error handling strategies |
vault-basic.yaml | Reference vault secrets with ${vault:KEY} syntax |
vault-sets.yaml | Auto-inject grouped secrets via variable sets |
otlp-jaeger.yaml | Export traces to Jaeger via OTLP |
π Stability
| Feature | Status | Compatibility |
|---|---|---|
| MCP gateway (stdio, SSE, HTTP) | Stable | Backward compatible in 0.x |
| Container orchestration (Docker) | Stable | Backward compatible in 0.x |
| Config schema (servers, resources) | Stable | Backward compatible in 0.x |
| Auth middleware (bearer, API key) | Stable | Backward compatible in 0.x |
| Hot reload | Stable | Backward compatible in 0.x |
| Vault secrets | Stable | Backward compatible in 0.x |
| Web UI | Stable | No API guarantee (internal) |
| Output format conversion | Stable | Backward compatible in 0.x |
| Token usage metrics | Stable | Backward compatible in 0.x |
| Stack validation (validate) | Stable | Backward compatible in 0.x |
| Stack planning (plan) | Stable | Backward compatible in 0.x |
| Static replicas | Stable | Backward compatible in 0.x |
| Reactive autoscaling | Experimental | May change without notice |
| Code mode | Experimental | May change without notice |
| Podman runtime | Stable | Backward compatible in 0.x |
| Skills registry workflows | Experimental | May change without notice |
| Skill acceptance criteria (test) | Experimental | May change without notice |
| Stack export (export) | Experimental | May change without notice |
| Spec drift detection | Experimental | May change without notice |
| Visual spec builder | Experimental | May change without notice |
| Skills import (skill add) | Experimental | May change without notice |
| Distributed tracing | Experimental | May change without notice |
β οΈ Known Limitations
- Podman rootless multi-container networking requires
netavarkandaardvark-dns(Podman 4.0+);pasta/slirp4netnsare egress-only transports and are not used for inter-container communication - Code mode sandbox has no filesystem access (by design)
- Skills registry is local-only with no remote discovery
- Web UI requires a modern browser (no IE11 support)
π Documentation
- Configuration Reference β every field in
stack.yaml - REST API Reference β all gateway endpoints
- Scaling β static replicas, reactive autoscaling, and the trade-offs
- Troubleshooting β common issues and resolutions
π€ Contributing
See CONTRIBUTING.md. We welcome PRs for new transport types, example stacks, and documentation improvements.
πͺͺ License
Built for engineers who'd rather be building and hate the absence of repeatable environments!
