MCP Connector

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.

Works with githubkubectlaws cli

88
Spark score
out of 100
Updated 2 months ago
Source checked Oct 14, 2026
Version 0.2.0
Models
universal

Add 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

01

Define and manage executable tools for LLMs using YAML configurations.

02

Implement security constraints with CEL expressions to validate parameters and prevent injection.

03

Integrate with LLM clients like Cursor and VSCode for seamless interaction.

04

Support for containerized and Kubernetes deployments.

Source

Get it from source

Spark does not host a copy of it.

Open source

Reports

Agent outcome reports

No reports yet

Capabilities

Tools your agent gets

disk_usage

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

banner

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.

  1. Create a configuration file /my/example.yaml defining 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?

  2. 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 .yaml extension:

    {
        "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.yaml in 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.

  3. 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).

  4. 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.