wtmcp

module
v0.1.7 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 5, 2026 License: GPL-3.0

README

wtmcp

MCP server with a language-agnostic plugin system. Plugins are simple executables (Python, bash, or any language) that communicate with the core over JSON-lines on stdin/stdout. The core handles auth, HTTP proxying, caching, and output encoding so plugins stay minimal.

Architecture

┌─────────────────────────────────────────────────┐
│  wtmcp (Go)                                     │
│                                                 │
│  MCP Server ─── Plugin Manager ─── HTTP Proxy   │
│  (mcp-go)       Discovery         Auth inject   │
│                 Lifecycle          SSRF protect │
│                 Dispatch           TLS verify   │
│                                    Rate limit   │
│                                                 │
│  Audit Log ─── Cache Store ─── Auth Providers   │
│  (JSON)        (memory/fs)     Bearer, Basic,   │
│                                Kerberos, OAuth2 │
│  Sandbox (optional)                             │
│  Landlock + cgroups + netns                     │
└────────┬──────────────────────────┬─────────────┘
         │ stdio (MCP/JSON-RPC)     │ stdin/stdout (JSON-lines)
    ┌────┴────┐               ┌─────┴──────────┐
    │ AI      │               │ Plugins        │
    │ Client  │               │ Zero deps      │
    └─────────┘               │ No HTTP libs   │
                              │ No auth code   │
                              └────────────────┘

Features

  • Plugin protocol: JSON-lines over stdin/stdout, any language
  • Auth: Bearer, Basic, Kerberos/SPNEGO, OAuth2 with token refresh, auto-detection from available credentials
  • HTTP proxy: Auth injection, domain validation, TLS enforcement, binary response encoding, multipart upload support
  • Cache: In-memory store with namespace isolation and TTL
  • Output: TOON encoding for ~40% token savings (optional)
  • Plugin setup: Manifest-declared wizard metadata for CLI tooling
  • Progressive discovery: Tools default to deferred; only primary tools are loaded into model context. Deferred tools are discoverable via tool_search and called directly through MCP
  • Encrypted credentials: Ansible Vault encrypted env.d files, auto-detected and decrypted transparently at startup

Security

wtmcp enforces security at the core level so plugins don't need to implement their own auth, input validation, or network restrictions.

HTTP Proxy & SSRF Prevention

All plugin HTTP traffic goes through the core proxy. No plugin makes direct network connections.

  • SSRF-safe dialer validates resolved IPs at connection time — blocks private, loopback, link-local, multicast, and IPv6-mapped IPv4 addresses
  • Domain allowlisting per plugin — only declared domains are reachable
  • Credential stripping on cross-domain redirects — Authorization, Cookie, and API key headers are removed when redirected to a different host
  • Dangerous headers stripped from plugin-crafted requests (Host, Proxy-Authorization, X-Forwarded-For, etc.)
  • HTTPS enforcement for authenticated requests; mTLS support with certificate chain verification
  • Userinfo URL rejectionuser:pass@host URLs are blocked
Sandboxing (optional)

Build with -tags sandbox to enable OS-level plugin isolation via arapuca:

  • Landlock LSM filesystem confinement — plugins can only read/write declared paths
  • cgroup v2 resource limits — memory, CPU, PIDs, file size (configurable per-plugin)
  • Network namespace isolation — plugins cannot make direct connections; all traffic routes through the core proxy
  • OOM detection and resource usage reporting after process exit
# Build with sandbox support (requires libarapuca)
make build-sandbox

# Default build works without libarapuca
make build

When sandbox is enabled in config but the binary lacks the tag, the server refuses to start with a clear error. When config uses the default (sandbox enabled implicitly), the server starts with a warning.

Rate Limiting

Token-bucket rate limiting with configurable per-plugin, per-domain, and global limits. Defaults: 120 req/min per plugin, 600 req/min global.

http:
  rate_limit:
    default: "120/m"
    global: "600/m"
    per_plugin:
      jira: "60/m"
    per_domain:
      api.github.com: "30/m"
HTTP Retries

Automatic retry with exponential backoff for transient upstream failures. Only idempotent methods (GET, HEAD, OPTIONS, PUT, DELETE) are retried — POST/PATCH are never retried. Respects Retry-After headers (clamped to 30s). Context-aware: the tool call timeout naturally caps total retry duration.

http:
  retries:
    max: 3                          # retries (not counting initial)
    backoff: exponential            # 1s, 2s, 4s... capped at 30s
    retry_on: [500, 502, 503, 504]  # status codes to retry
Cache Limits

Per-plugin entry limits with LRU eviction. Entries exceeding max_entry_size are rejected. Background cleanup removes expired entries at the configured interval.

cache:
  max_entries_per_plugin: 10000  # LRU eviction when exceeded
  max_entry_size: 1048576        # 1MB max per entry
  cleanup_interval: 60s          # expired entry sweep
Audit Logging

Structured JSON audit log with UUIDv7 correlation IDs:

  • Tool call events: plugin, tool, parameters (scrubbed), duration
  • Elicitation events: plugin, tool, action (accept/decline/cancel/ error/unsupported)
  • HTTP proxy events: method, host, path, status, response size
  • Credential scrubbing: field names (password, token, secret), JWT detection (eyJ prefix), high-entropy string detection
  • Configurable output: file (0600 permissions) and/or stdout
audit:
  log_file: logs/audit.log
  stdout: false
  scrub_fields: [password, token, secret, api_key, authorization]
Prompt Injection Defense
  • MCP Elicitation (enabled by default) prompts the user for confirmation before executing any write tool. The confirmation message shows the tool name and scrubbed parameters. Clients that lack elicitation support are blocked by default (elicitation_strict: true). Disable elicitation entirely with security.elicitation: false, or allow fallthrough for clients without elicitation support with security.elicitation_strict: false
  • Output framing (enabled by default) with per-session cryptographic nonce — injected tags in plugin output are detected and escaped. Disable with security.tag_tool_output: false
  • MCP Audience annotation set to [assistant] on all tool results (always active)
  • JSON Schema validation on every tool call from compiled plugin YAML
  • Write tool convention: included plugins default to dry_run=true in their schemas, requiring explicit opt-out. This is a plugin-level convention, not core-enforced
  • Read-only mode enforced at three layers: tool registration, disabled stubs, and runtime rejection
security:
  elicitation: true        # confirm before write tools (default: true)
  elicitation_strict: true # block writes if client lacks elicitation (default: true)
  tag_tool_output: true    # nonce-based output tagging (default: true)
Credential Isolation

Plugin processes receive only the credentials they need.

  • Scoped env.d — each plugin receives only its credential group's variables
  • Filtered environment — allowlist of safe system vars passed to plugins (PATH, HOME, LANG, TZ, TMPDIR, XDG_* dirs, etc.)
  • File permission enforcement — env.d files and directories require 0600/0700 (SSH-style)
  • Symlink rejection on credential files, CA certs, and env.d entries
  • Vault password zeroing — decryption keys cleared from memory after use (best-effort; Go's GC may retain copies)
  • Memory-backed secure files — decrypted credentials stored via memfd_create, never touch disk

Building and Running

make build

# Run with a workdir (default: ~/.config/wtmcp)
./wtmcp --workdir ~/.config/wtmcp

The workdir layout:

~/.config/wtmcp/
  config.yaml           Core config (optional)
  .env                  Environment variables
  env.d/*.env           Additional env files
  plugins/
    jira/
      plugin.yaml       Plugin manifest
      handler.py        Plugin executable

Writing Plugins

A plugin is a directory with a manifest (plugin.yaml) and a handler executable. The core discovers plugins, starts handlers as child processes, and routes tool calls over stdin/stdout using JSON-lines.

See docs/plugin-guide.md for the full guide with examples in multiple languages.

Minimal Example (bash)

A oneshot plugin that runs the handler once per tool call:

plugin.yaml:

name: hello
version: "1.0.0"
description: "A greeting plugin"
execution: oneshot
handler: ./handler.sh
tools:
  - name: hello_world
    description: "Says hello to someone"
    params:
      name:
        type: string
        default: "World"
        description: "Who to greet"
enabled: true

handler.sh:

#!/bin/bash
read -r INPUT
ID=$(echo "$INPUT" | jq -r '.id')
NAME=$(echo "$INPUT" | jq -r '.params.name // "World"')

echo "{}" | jq -c --arg id "$ID" --arg name "$NAME" \
  '{id: $id, type: "tool_result", result: {message: ("Hello, " + $name + "!")}}'
API Plugin Example (Python)

A persistent plugin that calls an API through the core's HTTP proxy. The handler stays running and processes multiple tool calls. Auth headers are injected automatically — the plugin never sees tokens.

plugin.yaml:

name: myapi
version: "1.0.0"
description: "Example API plugin"
execution: persistent
handler: ./handler.py

services:
  auth:
    type: bearer
    token: "${MY_API_TOKEN}"
  http:
    base_url: "${MY_API_URL}"

tools:
  - name: myapi_get_status
    description: "Get API status"
    params: {}
  - name: myapi_search
    description: "Search the API"
    params:
      query:
        type: string
        required: true
enabled: true

handler.py:

#!/usr/bin/env python3
import json, sys

def _send(msg):
    print(json.dumps(msg, separators=(",", ":")), flush=True)

def _recv():
    line = sys.stdin.readline()
    if not line:
        sys.exit(0)
    return json.loads(line.strip())

def http(method, path, query=None):
    msg = {"id": "1", "type": "http_request", "method": method, "path": path}
    if query:
        msg["query"] = query
    _send(msg)
    resp = _recv()
    return resp.get("status", 0), resp.get("body", {})

def get_status(_params):
    status, body = http("GET", "/status")
    return body

def search(params):
    status, body = http("GET", "/search", query={"q": params["query"]})
    return body

TOOLS = {"myapi_get_status": get_status, "myapi_search": search}

while True:
    msg = _recv()
    if msg.get("type") == "init":
        _send({"id": msg["id"], "type": "init_ok"})
    elif msg.get("type") == "shutdown":
        _send({"id": msg["id"], "type": "shutdown_ok"})
        break
    elif msg.get("type") == "tool_call":
        fn = TOOLS.get(msg.get("tool"))
        if fn:
            result = fn(msg.get("params", {}))
            _send({"id": msg["id"], "type": "tool_result", "result": result})
        else:
            _send({"id": msg["id"], "type": "tool_result",
                   "error": {"code": "unknown_tool", "message": msg.get("tool")}})
Key Concepts
  • Oneshot plugins are spawned per tool call. Simplest to write.
  • Persistent plugins start once and handle many calls via a main loop.
  • HTTP proxy: plugins send http_request messages, the core makes the call with auth and returns http_response. No HTTP library needed.
  • Cache: plugins send cache_get/cache_set messages. The core manages storage and TTL.
  • Auth variants: a single plugin can support multiple auth methods (e.g., Cloud Basic + Server Bearer + Kerberos) with auto-detection.

Plugin Management

Plugins can be reloaded at runtime without restarting the server.

From an AI assistant:

plugin_reload(name="jira")
plugin_list()

From a terminal (control directory):

touch ~/.config/wtmcp/control/commands/reload-jira
touch ~/.config/wtmcp/control/commands/reload-all

Results appear in ~/.config/wtmcp/control/results/. The server writes its PID to ~/.config/wtmcp/control/mcp.pid for process tracking.

MCP clients are automatically notified when tools or resources change.

OAuth Plugin Management

Plugin authentication (particularly for OAuth-enabled plugins) is managed through the wtmcpctl command-line utility. See README-wtmcpctl.md for usage instructions and setup.

Encrypted Credentials

env.d files can be encrypted with Ansible Vault for at-rest protection. The server auto-detects encrypted files by magic header and decrypts them transparently at startup. Plugins receive plaintext credentials as usual — no plugin changes needed.

Quick Start
# Create a vault password file (umask prevents brief permission race)
(umask 077 && openssl rand -base64 32 > ~/.vault-pass)

# Tell wtmcp where the password file is
# (add to ~/.config/wtmcp/config.yaml)
#   secrets:
#     vault_password_file: ~/.vault-pass

# Encrypt an env.d file
ansible-vault encrypt --vault-password-file ~/.vault-pass \
    ~/.config/wtmcp/env.d/jira.env

# Start the server — decrypts automatically
wtmcp

Encrypted files can be safely committed to git, shared, or backed up. Anyone who obtains them still needs the vault password to decrypt.

Password Sources

The vault password is resolved in priority order:

  1. WTMCP_VAULT_PASSWORD environment variable (CI/CD convenience)
  2. WTMCP_VAULT_PASSWORD_FILE environment variable (path to file)
  3. secrets.vault_password_file in config.yaml (recommended)

For production and workstations, prefer file-based passwords. Env vars are intended for CI/CD pipelines where mounting a file is inconvenient.

Multi-Password Support (Vault IDs)

Ansible Vault 1.2 supports labeled passwords (vault IDs). Different env.d files can use different passwords:

# Encrypt with a vault ID label
ansible-vault encrypt --vault-id prod@~/.vault-pass-prod \
    ~/.config/wtmcp/env.d/jira.env

Configure per-ID password files in config.yaml:

secrets:
  vault_password_file: ~/.vault-pass          # default
  vault_ids:
    prod: ~/.vault-pass-prod
    dev: ~/.vault-pass-dev

Per-ID env vars are also supported: WTMCP_VAULT_PASSWORD_PROD, WTMCP_VAULT_PASSWORD_DEV.

If no per-ID password is found, the server falls back to the default password chain automatically.

Diagnostics
wtmcp check

Reports vault password status and per-group encryption details (only encrypted groups are shown):

vault password: file (~/.vault-pass)
  - jira (encrypted, vault 1.1, decryption ok)
  - snyk (encrypted, vault 1.2 id=prod, decryption failed)
Migrating Existing Files
  1. Create a vault password file (see Quick Start)
  2. Configure secrets.vault_password_file in config.yaml
  3. Encrypt one env.d file: ansible-vault encrypt --vault-password-file ~/.vault-pass env.d/jira.env
  4. Verify: wtmcp check should show "decryption ok"
  5. Repeat for remaining files
  6. Optionally commit encrypted files to git

A single env.d directory can mix plaintext and encrypted files. Migrate incrementally — one file at a time.

If env.d files were previously committed in plaintext, encrypting them does not remove the plaintext from git history. Rotate the affected credentials after migrating and consider using git filter-repo to remove the old plaintext from history.

Reloading Encrypted Credentials

Credential changes take effect on plugin_reload without a server restart. The vault password is re-read from its source on each reload, so password rotations are picked up automatically.

Security Notes
  • Ansible Vault uses AES-256-CTR with PBKDF2-SHA256 (10,000 iterations). Use strong passwords (20+ characters or openssl rand -base64 32) to compensate for the low iteration count.
  • Back up your vault password file. Losing it means permanent loss of access to encrypted credentials. Store a copy in a separate secure location.
  • Ansible Vault is a practical improvement for development and CI/CD. Regulated environments requiring key rotation, audit logging, or FIPS-validated crypto should use HashiCorp Vault or cloud KMS.
Credential File Encryption

In addition to env.d files, credential files in credentials/<group>/ can also be vault-encrypted. Supported files:

  • client-credentials.json (OAuth2 client credentials)
  • TLS client_cert and client_key PEM files

Token files (token-*.json) are not encrypted — they are auto-rotated, short-lived, and derived from the client credentials.

Encrypted credential files are decrypted to memory-backed file descriptors (memfd on Linux, unlinked tmpfile on macOS) so decrypted content never touches persistent storage. Plugins receive the same file paths as usual — no plugin changes needed.

wtmcpctl vault Commands

Encrypt and decrypt files without requiring ansible-vault:

# Encrypt a file
wtmcpctl vault encrypt env.d/jira.env

# Encrypt with vault ID
wtmcpctl vault encrypt --vault-id prod env.d/jira.env

# Decrypt a file
wtmcpctl vault decrypt env.d/jira.env

# Verify decryption without writing
wtmcpctl vault decrypt --check env.d/jira.env

# View decrypted content without modifying the file
wtmcpctl vault view env.d/jira.env

Password is sourced from --vault-password-file, WTMCP_VAULT_PASSWORD env var, config.yaml, or interactive prompt (with echo suppression).

Included Plugins

Google Plugins

Google plugins provide access to Google Workspace services using OAuth2 authentication:

Plugin Description
google-drive File metadata, search, and export
google-calendar Calendar events and management
google-gmail Email reading and sending

All Google plugins require OAuth2 authentication. See README-wtmcpctl.md for setup instructions.

Jira Plugin

The included Jira plugin covers read, write, sprint, and export operations:

Category Examples
Read jira_search, jira_get_myself, jira_get_transitions
Write jira_create_issue, jira_add_comment, jira_assign_issue
Sprint jira_list_available_sprints, jira_get_sprint_issues
Export jira_export_sprint_data, jira_download_attachment

All write tools default to dry_run=true. Cloud-aware (ADF format, accountId assignments). Auth variants: Cloud Basic, Server Bearer, Server Kerberos.

Progressive Tool Discovery

By default (tools.discovery: full), all tools are loaded into the model's context. With progressive discovery, only primary tools are loaded; deferred tools are discoverable via tool_search.

Enable in config.yaml:

tools:
  discovery: progressive

Plugin authors mark key tools with visibility: primary in plugin.yaml. All other tools default to deferred. See docs/plugin-guide.md for details.

Testing

# Go core tests
go test ./...

# Go core tests with race detector
go test -race ./...

# Sandbox tests (requires libarapuca)
make test-sandbox

# Python plugin tests
.venv/bin/pytest tests/ -v

# All pre-commit checks
pre-commit run --all-files

Project Layout

cmd/
  wtmcp/                MCP server entry point
  wtmcpctl/             Plugin management CLI tool
internal/
  audit/                Structured JSON audit logging
  auth/                 Auth providers (bearer, basic, kerberos, oauth2)
  cache/                Key-value cache with TTL
  config/               Env var resolution, YAML config
  encoding/             TOON output encoding
  google/               Google OAuth helper (shared by Google plugins)
  plugin/               Manager, manifest, transport, dispatch
  protocol/             Wire protocol message types
  proxy/                HTTP proxy with SSRF prevention
  ratelimit/            Token-bucket rate limiting
  sandbox/              OS-level plugin isolation (optional)
  secrets/              Vault decryption, secure file descriptors
  server/               MCP server, output framing, tool index
  stats/                Per-tool call statistics
plugins/
  google-drive/         Google Drive plugin (Go)
  google-calendar/      Google Calendar plugin (Go)
  google-gmail/         Gmail plugin (Go)
  jira/                 Jira plugin (Python, zero external deps)
  confluence/           Confluence plugin (Python)
  gitlab/               GitLab plugin (Python)
tests/
  plugins/              Plugin unit tests
docs/
  plugin-guide.md       Plugin development guide
  wtmcpctl.md           OAuth management tool guide

License

This project is licensed under the GNU General Public License v3.0. See LICENSE for the full text.

Directories

Path Synopsis
cmd
wtmcp command
wtmcp is an MCP server with a language-agnostic plugin protocol.
wtmcp is an MCP server with a language-agnostic plugin protocol.
wtmcpctl command
wtmcpctl is a command-line utility for managing wtmcp plugins.
wtmcpctl is a command-line utility for managing wtmcp plugins.
internal
audit
Package audit provides structured security audit logging for tool invocations, HTTP proxy requests, and authentication events.
Package audit provides structured security audit logging for tool invocations, HTTP proxy requests, and authentication events.
auth
Package auth provides authentication providers for the HTTP proxy.
Package auth provides authentication providers for the HTTP proxy.
auth/kerberos
Package kerberos provides GSSAPI/SPNEGO authentication for HTTP requests.
Package kerberos provides GSSAPI/SPNEGO authentication for HTTP requests.
cache
Package cache provides a namespaced key-value cache for plugins.
Package cache provides a namespaced key-value cache for plugins.
config
Package config handles core configuration loading and environment variable resolution for wtmcp.
Package config handles core configuration loading and environment variable resolution for wtmcp.
encoding
Package encoding provides output format encoding for tool results.
Package encoding provides output format encoding for tool results.
google
Package google provides shared OAuth2 token loading for Google API plugins.
Package google provides shared OAuth2 token loading for Google API plugins.
plugin
Package plugin implements plugin process management, discovery, lifecycle, and the bidirectional JSON-lines transport.
Package plugin implements plugin process management, discovery, lifecycle, and the bidirectional JSON-lines transport.
pluginctx
Package pluginctx loads and serves plugin context/instruction files as MCP resources.
Package pluginctx loads and serves plugin context/instruction files as MCP resources.
protocol
Package protocol defines the wire protocol message types for bidirectional JSON-lines communication between core and plugins.
Package protocol defines the wire protocol message types for bidirectional JSON-lines communication between core and plugins.
proxy
Package proxy provides an HTTP proxy that makes authenticated requests on behalf of plugins.
Package proxy provides an HTTP proxy that makes authenticated requests on behalf of plugins.
ratelimit
Package ratelimit provides per-key token bucket rate limiting for tool calls and HTTP proxy requests.
Package ratelimit provides per-key token bucket rate limiting for tool calls and HTTP proxy requests.
sandbox
Package sandbox provides a stub implementation when built without the sandbox tag.
Package sandbox provides a stub implementation when built without the sandbox tag.
secrets/securefile
Package securefile creates memory-backed files readable by path.
Package securefile creates memory-backed files readable by path.
secrets/vault
Package vault implements Ansible Vault 1.1/1.2 encryption and decryption in pure Go.
Package vault implements Ansible Vault 1.1/1.2 encryption and decryption in pure Go.
server
Package server wires the MCP server to the plugin manager, registering tools from plugin manifests and serving via stdio.
Package server wires the MCP server to the plugin manager, registering tools from plugin manifests and serving via stdio.
stats
Package stats provides tool usage tracking and token estimation for wtmcp.
Package stats provides tool usage tracking and token estimation for wtmcp.
pkg
handler
Package handler provides helpers for writing Go plugin handlers that communicate with wtmcp via the JSON-lines protocol.
Package handler provides helpers for writing Go plugin handlers that communicate with wtmcp via the JSON-lines protocol.
plugins
gitlab command
gitlab handler is a persistent plugin for GitLab with multi-instance support.
gitlab handler is a persistent plugin for GitLab with multi-instance support.
google-calendar command
google-calendar handler is a persistent plugin for Google Calendar.
google-calendar handler is a persistent plugin for Google Calendar.
google-docs command
google-docs handler is a persistent plugin for Google Docs.
google-docs handler is a persistent plugin for Google Docs.
google-docs/highlighter
Package highlighter provides syntax highlighting for code blocks in Google Docs.
Package highlighter provides syntax highlighting for code blocks in Google Docs.
google-drive command
google-drive handler is a persistent plugin for Google Drive.
google-drive handler is a persistent plugin for Google Drive.
google-gmail command
google-gmail handler is a persistent plugin for Gmail.
google-gmail handler is a persistent plugin for Gmail.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL