Securely Execute OS Commands via LLM
MCP server that turns YAML-defined shell commands into constrained tools for LLMs, with CEL parameter validation and optional sandboxed runners.
0.2.0Add to Favorites
Why it matters
Enable Large Language Models to safely execute command-line operations on your operating system. This asset acts as a secure bridge, allowing LLMs to interact with your system's commands through a configurable and constraint-based interface.
Outcomes
What it gets done
Define and manage executable tools for LLMs using YAML configurations.
Implement security constraints with CEL expressions to validate parameters and prevent injection.
Integrate with LLM clients like Cursor and VSCode for seamless interaction.
Support for containerized and Kubernetes deployments.
Source
Get it from source
Spark does not host a copy of it.
Open sourceReports
Agent outcome reports
No reports yet
Capabilities
Tools your agent gets
Check disk usage for a directory with customizable depth analysis
Overview
MCPShell MCP Server
MCPShell is a tool that lets LLMs safely execute command-line tools through the Model Context Protocol. It provides a secure bridge between LLMs and operating system commands: you define tools in YAML with parameters, constraints and output formatting, and any MCP client can then call them. Use it to turn a shell command into an MCP tool, such as a disk-usage analyzer or Kubernetes and AWS CLI commands. Keep tools read-only and constrained, and do not add tools that remove or change things.
What it does
MCPShell is an MCP server that lets LLMs safely execute command-line tools through the Model Context Protocol, a secure bridge between LLMs and operating system commands. Any shell command can be run as an MCP tool, with parameter substitution through templates. Tools are defined in YAML with parameters, constraints and output formatting. Security comes from constraints, which validate tool parameters using CEL expressions before execution, and from optional sandboxed environments for running commands. Adding some shell code is enough to prototype a new MCP tool for your LLM. It works with any LLM client that supports MCP, including Cursor, VS Code and Witsy.
When to use - and when NOT to
Use it when you want an LLM to run specific command-line tasks under validated parameters, for instance a disk-usage analyzer, or letting the LLM know about a Kubernetes cluster with kubectl or run AWS CLI commands.
Do not create tools that change things, such as one that removes files. Limit tools to read-only actions, use constraints to restrict parameters to safe values, consider a sandboxed environment for running commands, review every command template for injection vulnerabilities and only expose tools that are safe for external use.
Capabilities
A tool file has an mcp block with a description and the shell to use, followed by a list of tools. A tool can have a name, description, typed parameters (with required flags and defaults), CEL constraints, a run block with a command template, a timeout, the environment variables to pass through and output formatting. Tools can declare required executables such as kubectl, and a tool's runners section can name sandbox-exec, firejail and a plain exec runner. The sandbox-exec and firejail runners take options such as allow_networking, allow_user_folders, allow_read_folders and allow_read_files.
MCPShell can also be deployed in containers and on Kubernetes.
How to install
Create a YAML tools file such as /my/example.yaml, then register the server in your MCP client. For Cursor, put a server entry in .cursor/mcp.json that runs the go command, so Go must be available, with the arguments run, github.com/inercia/MCPShell@v0.1.8, mcp, --tools and your file, plus --logfile with a log path. You can use a relative name without the .yaml extension, and MCPShell looks for it in the tools directory, ~/.mcpshell/tools/ by default:
{
"mcpServers": {
"mcp-cli-examples": {
"command": "go",
"args": [
"run", "github.com/inercia/MCPShell@v0.1.8",
"mcp", "--tools", "example",
"--logfile", "/some/path/mcpshell/example.log"
]
}
}
}
Cursor should recognize the server automatically the first time, but any change in the config file requires refreshing your MCP client. Then ask the LLM a question the new tool can answer. It can be configured in Cursor and Visual Studio Code, and other MCP clients are configured similarly. MIT licensed.
Source README
MCPShell
The MCPShell is a tool that allows LLMs to safely execute command-line tools
through the Model Context Protocol (MCP).
It provides a secure bridge between LLMs and operating system commands.
Features
- Flexible command execution: Run any shell commands as MCP tools,
with parameter substitution through templates. - Configuration-based tool definitions: Define tools in YAML with parameters,
constraints, and output formatting. - Security through constraints: Validate tool parameters using CEL expressions
before execution, as well as optional sanboxed environments
for running commands. - Quick proptotyping of MCP tools: just add some shell code and use it as
a MCP tool in your LLM. - Simple integration: Works with any LLM client supporting the MCP protocol
(ie, Cursor, VSCode, Witsy...)
Quick Start
Imagine you want Cursor (or some other MCP client) help you with your
space problems in your hard disk.
Create a configuration file
/my/example.yamldefining your tools:mcp: description: | Tool for analyzing disk usage to help identify what's consuming space. run: shell: bash tools: - name: "disk_usage" description: "Check disk usage for a directory" params: directory: type: string description: "Directory to analyze" required: true max_depth: type: number description: "Maximum depth to analyze (1-3)" default: 2 constraints: - "directory.startsWith('/')" # Must be absolute path - "!directory.contains('..')" # Prevent directory traversal - "max_depth >= 1 && max_depth <= 3" # Limit recursion depth - "directory.matches('^[\\w\\s./\\-_]+$')" # Only allow safe path characters, prevent command injection run: command: | du -h --max-depth={{ .max_depth }} {{ .directory }} | sort -hr | head -20 output: prefix: | Disk Usage Analysis (Top 20 largest directories):Take a look at the examples directory for more sophisticated and useful examples.
Maybe you prefer to let the LLM know about your Kubernetes cluster with
kubectl?
Or let it run some AWS CLI commands?Configure the MCP server in Cursor (or in any other LLM client with support for MCP)
For example, for Cursor, create
.cursor/mcp.json:{ // you need the "go" command available "mcpServers": { "mcp-cli-examples": { "command": "go", "args": [ "run", "github.com/inercia/MCPShell@v0.1.8", "mcp", "--tools", "/my/example.yaml", "--logfile", "/some/path/mcpshell/example.log" ] } } }You can also use relative paths and omit the
.yamlextension:{ "mcpServers": { "mcp-cli-examples": { "command": "go", "args": [ "run", "github.com/inercia/MCPShell@v0.1.8", "mcp", "--tools", "example", "--logfile", "/some/path/mcpshell/example.log" ] } } }This will look for
example.yamlin the tools directory (~/.mcpshell/tools/by default).See more details on how to configure Cursor or
Visual Studio Code. Other LLMs with support for MCPs
should be configured in a similar way.Make sure your MCP client is refreshed (Cursor should recognize it automatically the
firt time, but any change in the config file will require a refresh).Ask your LLM some questions it should be able to answer with the new tool. For example:
"I'm running out of space in my hard disk. Could you help me finding the problem?".
Usage and Configuration
Take a look at all the command in this document.
Configuration files use a YAML format defined here.
See the this directory for some examples.
For deploying MCPShell in containers and Kubernetes, see the Container Deployment Guide.
Agent Mode
For AI agent functionality that connects LLMs directly to tools, see the
Don project. Don provides:
- Direct LLM connectivity without requiring a separate MCP client
- RAG (Retrieval-Augmented Generation) support
- Multi-agent architecture
- Uses MCPShell's tool configuration format
Security Considerations
So you will probably thing
"this AI has helped me finding all those big files. What if I create another tool for removing files?".
Don't do that!.
- Limit the scope of these tools to read-only actions, do not give the LLM the power to change things.
- Use constraints to limit command execution to safe parameters
- Consider using a sanboxed environment for running commands.
- Review all command templates for potential injection vulnerabilities
- Only expose tools that are safe for external use
- All of the above!
Please read the Security Considerations document before using this software.
examples/kubectl-ro.yaml
mcp:
description: |
# Kubernetes Read-Only tools
Kubernetes Read-Only tools for enabling secure access to Kubernetes
cluster information, allowing users to list resources, view pod logs,
check cluster contexts, monitor resource usage, and inspect cluster
configuration without write permissions.
run:
shell: bash
tools:
- name: "kubectl_get"
description: |
List Kubernetes resources (pods, deployments, services, etc.)
Use the `kubectl_get_namespaces` command to list all namespaces.
Try to use some filtering for better performance and less noise.
requirements:
executables: ["kubectl"]
params:
resource:
type: string
description: "Resource type to list (pods, deployments, services, configmaps, secrets, etc.)"
required: true
context:
type: string
description: "Kubernetes context to use (required)"
required: true
ns:
type: string
description: "Kubernetes namespace (optional, uses current context's namespace if not specified)"
labels:
type: string
description: "Label selector to filter resources (e.g. 'app=nginx')"
output_format:
type: string
description: |
Output format (wide, json, yaml).
Try to use some formatting for better readability.
When looking for some specific resource, try to use the `wide` format.
filter_grep:
type: string
description: |
Filter logs with grep for the given string (literal text match).
filter_grep_regex:
type: string
description: |
Filter logs with grep using a regular expression pattern.
The pattern is an extended regular expression.
filter_jq:
type: string
description: |
Filter with jq for the given expression.
Make sure you use `output_format`` == 'json' to use this.
constraints:
- "resource.size() > 0" # Resource must not be empty
- "resource.size() <= 30" # Reasonable length limit
- "!resource.contains(' ')" # No spaces in resource name
- "!resource.contains(';')" # Prevent injection
- "context.size() > 0 && context.size() <= 253" # Valid context name (required)
- "ns == '' || (ns.size() <= 63 && ns.matches('^[a-z0-9]([-a-z0-9]*[a-z0-9])?$'))" # Valid k8s namespace
- "labels == '' || labels.size() <= 100" # Reasonable label length
- "output_format == '' || output_format == 'wide' || output_format == 'json' || output_format == 'yaml'" # Valid formats
run:
timeout: "60s"
command: |
NAMESPACE_PARAM="{{ if .ns }}-n {{ .ns }}{{ end }}"
LABEL_PARAM="{{ if .labels }}-l {{ .labels }}{{ end }}"
FORMAT_PARAM="{{ if .output_format }}-o {{ .output_format }}{{ end }}"
{{ if .filter_grep }}
kubectl --context={{ .context }} --request-timeout=30s get {{ .resource }} $NAMESPACE_PARAM $LABEL_PARAM $FORMAT_PARAM | grep -F "{{ .filter_grep }}" || true
{{ else if .filter_grep_regex }}
kubectl --context={{ .context }} --request-timeout=30s get {{ .resource }} $NAMESPACE_PARAM $LABEL_PARAM $FORMAT_PARAM | grep -E "{{ .filter_grep_regex }}" || true
{{ else if .filter_jq }}
kubectl --context={{ .context }} --request-timeout=30s get {{ .resource }} $NAMESPACE_PARAM $LABEL_PARAM $FORMAT_PARAM | jq "{{ .filter_jq }}" || true
{{ else }}
kubectl --context={{ .context }} --request-timeout=30s get {{ .resource }} $NAMESPACE_PARAM $LABEL_PARAM $FORMAT_PARAM
{{ end }}
env:
- KUBECONFIG
runners:
- name: sandbox-exec
options:
allow_networking: true
allow_user_folders: false
allow_read_folders:
- "/usr/bin"
- "/bin"
- "/etc"
- "{{ env \"HOME\" }}/.kube"
allow_read_files:
- "{{ env \"KUBECONFIG\" }}"
- name: firejail
options:
allow_networking: true
allow_user_folders: false
allow_read_folders:
- "/usr/bin"
- "/bin"
- "/etc"
- "{{ env \"HOME\" }}/.kube"
allow_read_files:
- "{{ env \"KUBECONFIG\" }}"
- name: exec
requirements: {}
output:
prefix: |
Kubernetes {{ .resource }} list:
- name: "kubectl_describe"
description: "Show detailed information about a Kubernetes resource"
requirements:
executables: ["kubectl"]
params:
resource:
type: string
description: "Kubernetes resource type (pods, deployments, services, etc.)"
required: true
name:
type: string
description: "Kubernetes resource name (optional, describes all resources of the type if not specified)"
ns:
type: string
description: "Kubernetes namespace (optional, uses current context's namespace if not specified)"
context:
type: string
description: "Kubernetes context to use (optional, uses current context if not specified)"
constraints:
- "resource.size() > 0" # Resource must not be empty
- "resource.size() <= 30" # Reasonable length limit
- "!resource.contains(' ')" # No spaces in resource name
- "!resource.contains(';')" # Prevent injection
- "context.size() > 0 && context.size() <= 253" # Valid context name (required)
- "name.size() <= 253" # Maximum DNS label length
- "name == '' || !name.contains(';')" # Prevent injection
- "ns == '' || (ns.size() <= 63 && ns.matches('^[a-z0-9]([-a-z0-9]*[a-z0-9])?$'))" # Valid k8s namespace
run:
timeout: "60s"
command: |
NAMESPACE_PARAM="{{ if .ns }}-n {{ .ns }}{{ end }}"
NAME_PARAM="{{ .name }}"
kubectl --context={{ .context }} --request-timeout=30s describe {{ .resource }} $NAME_PARAM $NAMESPACE_PARAM
env:
- KUBECONFIG
runners:
- name: sandbox-exec
options:
allow_networking: true
allow_user_folders: false
allow_read_folders:
- "/usr/bin"
- "/bin"
- "/etc"
- "{{ env \"HOME\" }}/.kube"
allow_read_files:
- "{{ env \"KUBECONFIG\" }}"
- name: firejail
options:
allow_networking: true
allow_user_folders: false
allow_read_folders:
- "/usr/bin"
- "/bin"
- "/etc"
- "{{ env \"HOME\" }}/.kube"
allow_read_files:
- "{{ env \"KUBECONFIG\" }}"
- name: exec
requirements: {}
output:
prefix: |
Details for {{ .resource }}{{ if .name }} "{{ .name }}"{{ end }}:
- name: "kubectl_logs"
description: |
Get logs from a pod.
Try to use some filtering for better performance and less noise.
requirements:
executables: ["kubectl"]
params:
pod:
type: string
description: "Pod name"
required: true
context:
type: string
description: "Kubernetes context to use (required)"
required: true
container:
type: string
description: "Container name (optional, uses the first container if not specified)"
ns:
type: string
description: "Kubernetes namespace (optional, uses current context's namespace if not specified)"
tail:
type: number
description: "Number of lines to show from the end (optional)"
previous:
type: boolean
description: "Show logs from previous container instance if it exists (optional)"
since:
type: string
description: "Show logs since relative time (e.g. '5s', '2m', or '3h')"
filter_grep:
type: string
description: |
Filter logs with grep for the given string (literal text match).
filter_grep_regex:
type: string
description: |
Filter logs with grep using a regular expression pattern.
constraints:
- "pod.size() > 0" # Pod name must not be empty
- "pod.size() <= 253" # Maximum DNS label length
- "!pod.contains(' ')" # No spaces in pod name
- "!pod.contains(';')" # Prevent injection
- "context.size() > 0 && context.size() <= 253" # Valid context name (required)
- "container == '' || (container.size() <= 253 && !container.contains(';'))" # Valid container name
- "ns == '' || (ns.size() <= 63 && ns.matches('^[a-z0-9]([-a-z0-9]*[a-z0-9])?$'))" # Valid k8s namespace
- "tail >= 0.0 && tail <= 10000.0" # Reasonable tail lines limit
- "since == '' || since.matches('^[0-9]+(s|m|h|d)$')" # Valid duration format
- "filter_grep == '' || (filter_grep.size() <= 100 && !filter_grep.contains(';'))" # Safe grep pattern
- "filter_grep_regex == '' || (filter_grep_regex.size() <= 100 && !filter_grep_regex.contains(';'))" # Safe grep regex pattern
- "(filter_grep == '' || filter_grep_regex == '')" # Only one of filter_grep or filter_grep_regex can be used
run:
timeout: "60s"
command: |
NAMESPACE_PARAM="{{ if .ns }}-n {{ .ns }}{{ end }}"
CONTAINER_PARAM="{{ if .container }}-c {{ .container }}{{ end }}"
TAIL_PARAM="{{ if .tail }}--tail={{ .tail }}{{ else }}--tail=100{{ end }}"
PREVIOUS_PARAM="{{ if .previous }}-p {{ end }}"
SINCE_PARAM="{{ if .since }}--since={{ .since }}{{ end }}"
# Base kubectl logs command
CMD="kubectl --context={{ .context }} --request-timeout=30s logs {{ .pod }} $NAMESPACE_PARAM $CONTAINER_PARAM $TAIL_PARAM $PREVIOUS_PARAM $SINCE_PARAM"
# Add grep filtering if specified
{{ if .filter_grep }}
$CMD | grep -F "{{ .filter_grep }}" || true
{{ else if .filter_grep_regex }}
$CMD | grep -E "{{ .filter_grep_regex }}" || true
{{ else }}
$CMD || true
{{ end }}
env:
- KUBECONFIG
runners:
- name: sandbox-exec
options:
allow_networking: true
allow_user_folders: false
allow_read_folders:
- "/usr/bin"
- "/bin"
- "/etc"
- "{{ env \"HOME\" }}/.kube"
allow_read_files:
- "{{ env \"KUBECONFIG\" }}"
- name: firejail
options:
allow_networking: true
allow_user_folders: false
allow_read_folders:
- "/usr/bin"
- "/bin"
- "/etc"
- "{{ env \"HOME\" }}/.kube"
allow_read_files:
- "{{ env \"KUBECONFIG\" }}"
- name: exec
requirements: {}
output:
prefix: |
Logs for pod {{ .pod }}{{ if .container }} (container: {{ .container }}){{ end }}{{ if .grep }} (filtered for literal: '{{ .grep }}'){{ end }}{{ if .grep_regex }} (filtered for regex: '{{ .grep_regex }}'){{ end }}:
- name: "kubectl_get_contexts"
description: "List available Kubernetes contexts"
requirements:
executables: ["kubectl"]
params:
context:
type: string
description: "Kubernetes context to use (optional, uses current context if not specified)"
run:
timeout: "60s"
command: |
CONTEXT_PARAM="{{ if .context }}--context={{ .context }}{{ end }}"
kubectl $CONTEXT_PARAM --request-timeout=30s config get-contexts
env:
- KUBECONFIG
runners:
- name: sandbox-exec
options:
allow_networking: true
allow_user_folders: false
allow_read_folders:
- "/usr/bin"
- "/bin"
- "/etc"
- "{{ env \"HOME\" }}/.kube"
allow_read_files:
- "{{ env \"KUBECONFIG\" }}"
- name: firejail
options:
allow_networking: true
allow_user_folders: false
allow_read_folders:
- "/usr/bin"
- "/bin"
- "/etc"
- "{{ env \"HOME\" }}/.kube"
allow_read_files:
- "{{ env \"KUBECONFIG\" }}"
- name: exec
requirements: {}
output:
prefix: |
Available Kubernetes contexts for user {{ env "USER" }}:
- name: "kubectl_current_context"
description: "Show the current Kubernetes context"
requirements:
executables: ["kubectl"]
params:
context:
type: string
description: "Kubernetes context to use (optional, uses current context if not specified)"
run:
timeout: "60s"
command: |
CONTEXT_PARAM="{{ if .context }}--context={{ .context }}{{ end }}"
kubectl $CONTEXT_PARAM --request-timeout=30s config current-context
env:
- KUBECONFIG
runners:
- name: sandbox-exec
options:
allow_networking: true
allow_user_folders: false
allow_read_folders:
- "/usr/bin"
- "/bin"
- "/etc"
- "{{ env \"HOME\" }}/.kube"
allow_read_files:
- "{{ env \"KUBECONFIG\" }}"
- name: firejail
options:
allow_networking: true
allow_user_folders: false
allow_read_folders:
- "/usr/bin"
- "/bin"
- "/etc"
- "{{ env \"HOME\" }}/.kube"
allow_read_files:
- "{{ env \"KUBECONFIG\" }}"
- name: exec
requirements: {}
output:
prefix: |
Current Kubernetes context:
- name: "kubectl_get_namespaces"
description: "List all namespaces in the cluster"
requirements:
executables: ["kubectl"]
params:
context:
type: string
description: "Kubernetes context to use (required)"
required: true
constraints:
- "context.size() > 0 && context.size() <= 253" # Valid context name (required)
run:
timeout: "60s"
command: |
kubectl --context={{ .context }} --request-timeout=30s get namespaces
env:
- KUBECONFIG
runners:
- name: sandbox-exec
options:
allow_networking: true
allow_user_folders: false
allow_read_folders:
- "/usr/bin"
- "/bin"
- "/etc"
- "{{ env \"HOME\" }}/.kube"
allow_read_files:
- "{{ env \"KUBECONFIG\" }}"
- name: firejail
options:
allow_networking: true
allow_user_folders: false
allow_read_folders:
- "/usr/bin"
- "/bin"
- "/etc"
- "{{ env \"HOME\" }}/.kube"
allow_read_files:
- "{{ env \"KUBECONFIG\" }}"
- name: exec
requirements: {}
output:
prefix: |
Available Kubernetes namespaces:
- name: "kubectl_portforward_curl"
description: "Create a port forward to a Pod:Port and run curl to that target"
requirements:
executables:
- "kubectl"
- "nc"
- "curl"
- "jq"
- "grep"
params:
context:
type: string
description: "Kubernetes context to use (required)"
required: true
pod:
type: string
description: "Pod name"
required: true
ns:
type: string
description: "Kubernetes namespace (optional, uses current context's namespace if not specified)"
pod_port:
type: string
description: "Remote port in the Pod to connect to (e.g. 8080). Only one port can be provided."
required: true
local_port:
type: number
description: "Local port to bind to (e.g. 54320). Only one port can be provided. A defautl one will be used if not specified"
curl_args:
type: string
description: "Additional arguments to pass to curl (e.g. '-I' for headers only)"
path:
type: string
description: "URL's path (e.g. '/index.html'). Make sure it starts with a '/'."
filter_jq:
type: string
description: "jq expression to apply to the response (optional)"
filter_grep:
type: string
description: "grep expression to apply to the response (optional, literal text match)"
filter_grep_regex:
type: string
description: "grep regex pattern to apply to the response (optional)"
constraints:
- "context.size() > 0 && context.size() <= 253" # Valid context name (required)
- "ns == '' || (ns.size() <= 63 && ns.matches('^[a-z0-9]([-a-z0-9]*[a-z0-9])?$'))" # Valid k8s namespace
- "pod_port.size() > 0" # Port must not be empty
- "pod_port.matches('^[0-9]+$')" # Port must be a number
- "path.size() > 0" # Path must not be empty
- "path.matches('^/[^ ]+$')" # Path must start with a '/' and contain no spaces
- "filter_grep == '' || filter_grep_regex == ''" # Only one of filter_grep or filter_grep_regex can be used
- "filter_jq == '' || (filter_grep == '' && filter_grep_regex == '')" # Can't use both jq and grep filters
run:
timeout: "60s"
command: |
NAMESPACE_PARAM="{{ if .ns }}-n {{ .ns }}{{ end }}"
CURL_ARGS="-s {{ if .curl_args }}{{ .curl_args }}{{ end }}"
localport="{{ if .local_port }}{{ .local_port }}{{ else }}54320{{ end }}"
remoteport="{{ .pod_port }}"
# nmap -sT -p $localport localhost || true # This would show that the port is closed
kubectl --context={{ .context }} port-forward $NAMESPACE_PARAM pod/{{ .pod }} $localport:$remoteport >/dev/null &
pid=$!
trap '{
kill $pid
}' EXIT
# wait for $localport to become available
while ! nc -vz localhost $localport > /dev/null 2>&1 ; do
sleep 0.1
done
# nmap -sT -p $localport localhost # This would show that the port is open
{{ if .filter_jq }}
curl $CURL_ARGS "http://localhost:$localport{{ .path }}" | jq '{{ .filter_jq }}' 2>/dev/null || true
{{ else if .filter_grep }}
curl $CURL_ARGS "http://localhost:$localport{{ .path }}" | grep -F "{{ .filter_grep }}" || true
{{ else if .filter_grep_regex }}
curl $CURL_ARGS "http://localhost:$localport{{ .path }}" | grep -E "{{ .filter_grep_regex }}" || true
{{ else }}
curl $CURL_ARGS "http://localhost:$localport{{ .path }}"
{{ end }}
# the 'trap ... EXIT' above will take care of kill $pid
env:
- KUBECONFIG
runners:
- name: sandbox-exec
options:
allow_networking: true
allow_user_folders: false
allow_read_folders:
- "/usr/bin"
- "/bin"
- "/etc"
- "/usr/local/bin"
- "/opt/homebrew/bin"
- "{{ env \"HOME\" }}/.kube"
allow_read_files:
- "{{ env \"KUBECONFIG\" }}"
- name: firejail
options:
allow_networking: true
allow_user_folders: false
allow_read_folders:
- "/usr/bin"
- "/bin"
- "/etc"
- "/usr/local/bin"
- "/opt/homebrew/bin"
- "{{ env \"HOME\" }}/.kube"
allow_read_files:
- "{{ env \"KUBECONFIG\" }}"
- name: exec
requirements: {}
output:
prefix: |
Port forwarding to pod {{ .pod }}:{{ .pod_port }}{{ if .filter_grep }} (filtered for literal: '{{ .filter_grep }}'){{ end }}{{ if .filter_grep_regex }} (filtered for regex: '{{ .filter_grep_regex }}'){{ end }}:
- name: "kubectl_envoy"
description: |
Get some configuration information from Envoy by running a port forward
to the given Envoy pod and then using curl to access the desired path.
Some of the most useful paths are:
/certs: List out all loaded TLS certificates, including file name, serial number, subject alternate names and days until expiration in JSON format conforming to the certificate proto definition.
/clusters: List out all configured cluster manager clusters. This information includes all discovered upstream hosts in each cluster along with per host statistics. This is useful for debugging service discovery issues.
/clusters?format=json: List out all configured cluster manager clusters in JSON format.
/config_dump: Dump currently loaded configuration from various Envoy components as JSON-serialized proto messages. See the response definition for more information.
/config_dump?resource=RESOURCE: Dump only the currently loaded configuration that matches the specified resource. The resource must be a repeated field in one of the top level config dumps such as static_listeners from ListenersConfigDump or dynamic_active_clusters from ClustersConfigDump. If you need a non-repeated field, use the mask query parameter documented above. If you want only a subset of fields from the repeated resource, use both as documented below.
/config_dump?name_regex=REGEX: Dump only the currently loaded configurations whose names match the specified regex. Can be used with both resource and mask query parameters.
/listeners: List out all configured listeners. This information includes the names of listeners as well as the addresses that they are listening on. If a listener is configured to listen on port 0, then the output will contain the actual port that was allocated by the OS.
/listeners?format=json: List out all configured listeners in JSON format.
/server_info: Get the server information for the Envoy instance.
/stats: Get the stats for the Envoy instance.
/stats?filter=REGEX: Filters the returned stats to those with names matching the regular expression regex. Compatible with usedonly. Performs partial matching by default, so /stats?filter=server will return all stats containing the word server. Full-string matching can be specified with begin- and end-line anchors. (i.e. /stats?filter=^server.concurrency$).
/stats?format=json: List out all stats in JSON format.
/stats/recentlookups: This endpoint helps Envoy developers debug potential contention issues in the stats system. Initially, only the count of StatName lookups is acumulated, not the specific names that are being looked up. In order to see specific recent requests, you must enable the feature by POSTing to /stats/recentlookups/enable. There may be approximately 40-100 nanoseconds of added overhead per lookup.
Information provided by these paths is useful for debugging Envoy configuration
and service discovery issues.
This information is usually VERY VERBOSE. Try to filter it with grep or jq to get
the information you need. Do not try to show or process this information as it is,
or you will end up with a lot of noise and a lot of data to process.
Read the Envoy documentation located at https://www.envoyproxy.io/docs/envoy/latest/operations/admin
for more information about these Envoy paths and the information they provide.
requirements:
executables:
- "kubectl"
- "nc"
- "curl"
- "jq"
- "grep"
params:
context:
type: string
description: "Kubernetes context to use (required)"
required: true
pod:
type: string
description: "Pod name"
required: true
ns:
type: string
description: "Kubernetes namespace (optional, uses current context's namespace if not specified)"
pod_port:
type: string
description: "Remote port in the Pod to connect to (e.g. 8080). Only one port can be provided."
required: true
local_port:
type: number
description: "Local port to bind to (e.g. 54320). Only one port can be provided. A defautl one will be used if not specified"
curl_args:
type: string
description: "Additional arguments to pass to curl (e.g. '-I' for headers only)"
path:
type: string
description: "URL's path (e.g. '/index.html'). Make sure it starts with a '/'."
filter_jq:
type: string
description: "jq expression to apply to the response (optional)"
filter_grep:
type: string
description: "grep expression to apply to the response (optional, literal text match)"
filter_grep_regex:
type: string
description: "grep regex pattern to apply to the response (optional)"
constraints:
- "context.size() > 0 && context.size() <= 253" # Valid context name (required)
- "ns == '' || (ns.size() <= 63 && ns.matches('^[a-z0-9]([-a-z0-9]*[a-z0-9])?$'))" # Valid k8s namespace
- "pod_port.size() > 0" # Port must not be empty
- "pod_port.matches('^[0-9]+$')" # Port must be a number
- "path.size() > 0" # Path must not be empty
- "path.matches('^/[^ ]+$')" # Path must start with a '/' and contain no spaces
- "filter_grep == '' || filter_grep_regex == ''" # Only one of filter_grep or filter_grep_regex can be used
- "filter_jq == '' || (filter_grep == '' && filter_grep_regex == '')" # Can't use both jq and grep filters
run:
timeout: "60s"
command: |
NAMESPACE_PARAM="{{ if .ns }}-n {{ .ns }}{{ end }}"
CURL_ARGS="-s {{ if .curl_args }}{{ .curl_args }}{{ end }}"
localport="{{ if .local_port }}{{ .local_port }}{{ else }}54320{{ end }}"
remoteport="{{ .pod_port }}"
# nmap -sT -p $localport localhost || true # This would show that the port is closed
kubectl --context={{ .context }} port-forward $NAMESPACE_PARAM pod/{{ .pod }} $localport:$remoteport >/dev/null &
pid=$!
trap '{
kill $pid
}' EXIT
# wait for $localport to become available
while ! nc -vz localhost $localport > /dev/null 2>&1 ; do
sleep 0.1
done
# nmap -sT -p $localport localhost # This would show that the port is open
{{ if .filter_jq }}
curl $CURL_ARGS "http://localhost:$localport{{ .path }}" | jq '{{ .filter_jq }}' || true
{{ else if .filter_grep }}
curl $CURL_ARGS "http://localhost:$localport{{ .path }}" | grep -F '{{ .filter_grep }}' || true
{{ else if .filter_grep_regex }}
curl $CURL_ARGS "http://localhost:$localport{{ .path }}" | grep -E '{{ .filter_grep_regex }}' || true
{{ else }}
curl $CURL_ARGS "http://localhost:$localport{{ .path }}"
{{ end }}
# the 'trap ... EXIT' above will take care of kill $pid
env:
- KUBECONFIG
runners:
- name: sandbox-exec
requirements:
os: darwin
executables: [sandbox-exec]
options:
allow_networking: true
allow_user_folders: false
allow_read_folders:
- "/usr/bin"
- "/bin"
- "/etc"
- "/usr/local/bin"
- "/opt/homebrew/bin"
- "{{ env \"HOME\" }}/.kube"
allow_read_files:
- "{{ env \"KUBECONFIG\" }}"
- name: firejail
options:
allow_networking: true
allow_user_folders: false
allow_read_folders:
- "/usr/bin"
- "/bin"
- "/etc"
- "/usr/local/bin"
- "/opt/homebrew/bin"
- "{{ env \"HOME\" }}/.kube"
allow_read_files:
- "{{ env \"KUBECONFIG\" }}"
- name: exec
requirements: {}
output:
prefix: |
Envoy configuration from pod {{ .pod }}:{{ .pod_port }}{{ .path }}{{ if .filter_grep }} (filtered for literal: '{{ .filter_grep }}'){{ end }}{{ if .filter_grep_regex }} (filtered for regex: '{{ .filter_grep_regex }}'){{ end }}:
examples/aws-networking-ro.yaml
mcp:
description: |
AWS Networking Diagnostics Toolkit providing read-only access to AWS network configuration
and status information. This toolkit helps troubleshoot connectivity issues, security group
configurations, VPC setups, routing problems, and other AWS network-related concerns
through a secure, constrained interface to the AWS CLI.
NOTE FOR LLM: Authentication with AWS CLI is required to use these tools. If commands fail
with authentication errors, use the "aws_list_profiles" tool if available, and suggest
to use one of those profiles. If this tool is not available, please suggest to
add the tools in the "aws-ro.yaml" configuration, that provide:
1. aws_list_profiles - To list and manage AWS profiles
2. Use a specific profile with the 'profile' parameter on any command
run:
shell: bash
tools:
- name: "aws_vpc_list"
description: "List all VPCs in the AWS account"
params:
region:
type: string
description: "AWS region (e.g., us-east-1, eu-west-1)"
required: true
profile:
type: string
description: "AWS profile to use (optional, uses default if not specified)"
output_format:
type: string
description: "Output format (json, text, table)"
constraints:
- "region.matches('^[a-z]{2}-[a-z]+-[0-9]{1}$')" # Valid AWS region format
- "profile == '' || profile.matches('^[a-zA-Z0-9_-]+$')" # Valid profile name format
- "output_format == '' || ['json', 'text', 'table'].exists(f, f == output_format)" # Valid output formats
run:
timeout: "60s"
env:
- AWS_PROFILE
- AWS_DEFAULT_PROFILE
- AWS_SHARED_CREDENTIALS_FILE
- HOME
- AWS_CONFIG_FILE
command: |
FORMAT_PARAM="{{ if .output_format }}--output {{ .output_format }}{{ else }}--output table{{ end }}"
PROFILE_PARAM="{{ if .profile }}--profile {{ .profile }}{{ end }}"
# Fix environment variable names and handling
if [ -z "$AWS_SHARED_CREDENTIALS_FILE" ]; then
export AWS_SHARED_CREDENTIALS_FILE="$HOME/.aws/credentials"
fi
if [ -z "$AWS_CONFIG_FILE" ]; then
export AWS_CONFIG_FILE="$HOME/.aws/config"
fi
echo "Using AWS_SHARED_CREDENTIALS_FILE: $AWS_SHARED_CREDENTIALS_FILE"
echo "Using AWS_CONFIG_FILE: $AWS_CONFIG_FILE"
# Attempt to run the command and capture the output and exit status
OUTPUT=$(aws $PROFILE_PARAM ec2 describe-vpcs --region {{ .region }} $FORMAT_PARAM 2>&1)
EXIT_CODE=$?
# Check if authentication failed
if [[ $EXIT_CODE -ne 0 && ($OUTPUT == *"Unable to locate credentials"* || $OUTPUT == *"AuthFailure"* || $OUTPUT == *"ExpiredToken"* || $OUTPUT == *"AccessDenied"*) ]]; then
echo "AWS authentication error detected. You need valid AWS credentials to use this tool."
echo ""
echo "Try using the aws_auth_check tool from aws-ro.yaml to diagnose authentication issues:"
echo "aws_auth_check region={{ .region }}{{ if .profile }} profile={{ .profile }}{{ end }}"
echo ""
echo "Error details: $OUTPUT"
exit 1
else
# If no authentication error, just output the result
echo "$OUTPUT"
fi
output:
prefix: "VPCs in region {{ .region }}:"
- name: "aws_subnet_list"
description: "List all subnets in a VPC"
params:
region:
type: string
description: "AWS region (e.g., us-east-1, eu-west-1)"
required: true
profile:
type: string
description: "AWS profile to use (optional, uses default if not specified)"
vpc_id:
type: string
description: "VPC ID (e.g., vpc-12345678)"
output_format:
type: string
description: "Output format (json, text, table)"
constraints:
- "region.matches('^[a-z]{2}-[a-z]+-[0-9]{1}$')" # Valid AWS region format
- "profile == '' || profile.matches('^[a-zA-Z0-9_-]+$')" # Valid profile name format
- "vpc_id == '' || vpc_id.matches('^vpc-[a-f0-9]{8,}$')" # Valid VPC ID format
- "output_format == '' || ['json', 'text', 'table'].exists(f, f == output_format)" # Valid output formats
run:
timeout: "60s"
env:
- AWS_PROFILE
- AWS_DEFAULT_PROFILE
- AWS_SHARED_CREDENTIALS_FILE
- HOME
- AWS_CONFIG_FILE
command: |
FORMAT_PARAM="{{ if .output_format }}--output {{ .output_format }}{{ else }}--output table{{ end }}"
PROFILE_PARAM="{{ if .profile }}--profile {{ .profile }}{{ end }}"
FILTER="{{ if .vpc_id }}--filters Name=vpc-id,Values={{ .vpc_id }}{{ end }}"
# Fix environment variable names and handling
if [ -z "$AWS_SHARED_CREDENTIALS_FILE" ]; then
export AWS_SHARED_CREDENTIALS_FILE="$HOME/.aws/credentials"
fi
if [ -z "$AWS_CONFIG_FILE" ]; then
export AWS_CONFIG_FILE="$HOME/.aws/config"
fi
echo "Using AWS_SHARED_CREDENTIALS_FILE: $AWS_SHARED_CREDENTIALS_FILE"
echo "Using AWS_CONFIG_FILE: $AWS_CONFIG_FILE"
aws $PROFILE_PARAM ec2 describe-subnets --region {{ .region }} $FILTER $FORMAT_PARAM
output:
prefix: "Subnets{{ if .vpc_id }} in VPC {{ .vpc_id }}{{ end }} (region {{ .region }}):"
- name: "aws_security_group_list"
description: "List all security groups, optionally filtered by VPC"
params:
region:
type: string
description: "AWS region (e.g., us-east-1, eu-west-1)"
required: true
profile:
type: string
description: "AWS profile to use (optional, uses default if not specified)"
vpc_id:
type: string
description: "VPC ID (e.g., vpc-12345678)"
output_format:
type: string
description: "Output format (json, text, table)"
constraints:
- "region.matches('^[a-z]{2}-[a-z]+-[0-9]{1}$')" # Valid AWS region format
- "profile == '' || profile.matches('^[a-zA-Z0-9_-]+$')" # Valid profile name format
- "vpc_id == '' || vpc_id.matches('^vpc-[a-f0-9]{8,}$')" # Valid VPC ID format
- "output_format == '' || ['json', 'text', 'table'].exists(f, f == output_format)" # Valid output formats
run:
timeout: "60s"
env:
- AWS_PROFILE
- AWS_DEFAULT_PROFILE
- AWS_SHARED_CREDENTIALS_FILE
- HOME
- AWS_CONFIG_FILE
command: |
FORMAT_PARAM="{{ if .output_format }}--output {{ .output_format }}{{ else }}--output table{{ end }}"
PROFILE_PARAM="{{ if .profile }}--profile {{ .profile }}{{ end }}"
FILTER="{{ if .vpc_id }}--filters Name=vpc-id,Values={{ .vpc_id }}{{ end }}"
# Fix environment variable names and handling
if [ -z "$AWS_SHARED_CREDENTIALS_FILE" ]; then
export AWS_SHARED_CREDENTIALS_FILE="$HOME/.aws/credentials"
fi
if [ -z "$AWS_CONFIG_FILE" ]; then
export AWS_CONFIG_FILE="$HOME/.aws/config"
fi
echo "Using AWS_SHARED_CREDENTIALS_FILE: $AWS_SHARED_CREDENTIALS_FILE"
echo "Using AWS_CONFIG_FILE: $AWS_CONFIG_FILE"
aws $PROFILE_PARAM ec2 describe-security-groups --region {{ .region }} $FILTER $FORMAT_PARAM
output:
prefix: "Security groups{{ if .vpc_id }} in VPC {{ .vpc_id }}{{ end }} (region {{ .region }}):"
- name: "aws_security_group_detail"
description: "Get detailed information about a specific security group"
params:
region:
type: string
description: "AWS region (e.g., us-east-1, eu-west-1)"
required: true
profile:
type: string
description: "AWS profile to use (optional, uses default if not specified)"
group_id:
type: string
description: "Security group ID (e.g., sg-12345678)"
required: true
output_format:
type: string
description: "Output format (json, text, table)"
constraints:
- "region.matches('^[a-z]{2}-[a-z]+-[0-9]{1}$')" # Valid AWS region format
- "profile == '' || profile.matches('^[a-zA-Z0-9_-]+$')" # Valid profile name format
- "group_id.matches('^sg-[a-f0-9]{8,}$')" # Valid security group ID format
- "output_format == '' || ['json', 'text', 'table'].exists(f, f == output_format)" # Valid output formats
run:
timeout: "60s"
env:
- AWS_PROFILE
- AWS_DEFAULT_PROFILE
- AWS_SHARED_CREDENTIALS_FILE
- HOME
- AWS_CONFIG_FILE
command: |
FORMAT_PARAM="{{ if .output_format }}--output {{ .output_format }}{{ else }}--output table{{ end }}"
PROFILE_PARAM="{{ if .profile }}--profile {{ .profile }}{{ end }}"
# Fix environment variable names and handling
if [ -z "$AWS_SHARED_CREDENTIALS_FILE" ]; then
export AWS_SHARED_CREDENTIALS_FILE="$HOME/.aws/credentials"
fi
if [ -z "$AWS_CONFIG_FILE" ]; then
export AWS_CONFIG_FILE="$HOME/.aws/config"
fi
echo "Using AWS_SHARED_CREDENTIALS_FILE: $AWS_SHARED_CREDENTIALS_FILE"
echo "Using AWS_CONFIG_FILE: $AWS_CONFIG_FILE"
aws $PROFILE_PARAM ec2 describe-security-groups --region {{ .region }} --group-ids {{ .group_id }} $FORMAT_PARAM
output:
prefix: "Security group {{ .group_id }} details (region {{ .region }}):"
- name: "aws_nacl_list"
description: "List network ACLs, optionally filtered by VPC"
params:
region:
type: string
description: "AWS region (e.g., us-east-1, eu-west-1)"
required: true
profile:
type: string
description: "AWS profile to use (optional, uses default if not specified)"
vpc_id:
type: string
description: "VPC ID (e.g., vpc-12345678)"
output_format:
type: string
description: "Output format (json, text, table)"
constraints:
- "region.matches('^[a-z]{2}-[a-z]+-[0-9]{1}$')" # Valid AWS region format
- "profile == '' || profile.matches('^[a-zA-Z0-9_-]+$')" # Valid profile name format
- "vpc_id == '' || vpc_id.matches('^vpc-[a-f0-9]{8,}$')" # Valid VPC ID format
- "output_format == '' || ['json', 'text', 'table'].exists(f, f == output_format)" # Valid output formats
run:
timeout: "60s"
env:
- AWS_PROFILE
- AWS_DEFAULT_PROFILE
- AWS_SHARED_CREDENTIALS_FILE
- HOME
- AWS_CONFIG_FILE
command: |
FORMAT_PARAM="{{ if .output_format }}--output {{ .output_format }}{{ else }}--output table{{ end }}"
PROFILE_PARAM="{{ if .profile }}--profile {{ .profile }}{{ end }}"
FILTER="{{ if .vpc_id }}--filters Name=vpc-id,Values={{ .vpc_id }}{{ end }}"
# Fix environment variable names and handling
if [ -z "$AWS_SHARED_CREDENTIALS_FILE" ]; then
export AWS_SHARED_CREDENTIALS_FILE="$HOME/.aws/credentials"
fi
if [ -z "$AWS_CONFIG_FILE" ]; then
export AWS_CONFIG_FILE="$HOME/.aws/config"
fi
echo "Using AWS_SHARED_CREDENTIALS_FILE: $AWS_SHARED_CREDENTIALS_FILE"
echo "Using AWS_CONFIG_FILE: $AWS_CONFIG_FILE"
aws $PROFILE_PARAM ec2 describe-network-acls --region {{ .region }} $FILTER $FORMAT_PARAM
output:
prefix: "Network ACLs{{ if .vpc_id }} in VPC {{ .vpc_id }}{{ end }} (region {{ .region }}):"
- name: "aws_route_table_list"
description: "List route tables, optionally filtered by VPC"
params:
region:
type: string
description: "AWS region (e.g., us-east-1, eu-west-1)"
required: true
profile:
type: string
description: "AWS profile to use (optional, uses default if not specified)"
vpc_id:
type: string
description: "VPC ID (e.g., vpc-12345678)"
output_format:
type: string
description: "Output format (json, text, table)"
constraints:
- "region.matches('^[a-z]{2}-[a-z]+-[0-9]{1}$')" # Valid AWS region format
- "profile == '' || profile.matches('^[a-zA-Z0-9_-]+$')" # Valid profile name format
- "vpc_id == '' || vpc_id.matches('^vpc-[a-f0-9]{8,}$')" # Valid VPC ID format
- "output_format == '' || ['json', 'text', 'table'].exists(f, f == output_format)" # Valid output formats
run:
timeout: "60s"
env:
- AWS_PROFILE
- AWS_DEFAULT_PROFILE
- AWS_SHARED_CREDENTIALS_FILE
- HOME
- AWS_CONFIG_FILE
command: |
FORMAT_PARAM="{{ if .output_format }}--output {{ .output_format }}{{ else }}--output table{{ end }}"
PROFILE_P...
FAQ
Common questions
Discussion
Questions & comments · 0
Sign In Sign in to leave a comment.