Skip to main content
Glama
yujoonko1

relay-mcp

by yujoonko1

relay-mcp

CI PyPI Python License: MIT

Expose any dev tool as a safe, audited MCP server — in about 10 lines.

pip install relay-mcp

Model Context Protocol (MCP) is how AI agents and IDEs connect to external tools. Writing an MCP server by hand means hand-rolling JSON schemas, wiring transport handlers, and — critically — deciding what happens when an agent asks to do something destructive.

Relay handles all of that:

  • @tool decorator — turns a plain typed Python function into an MCP tool. Input schema is generated from type hints; parameter docs come from your docstring.

  • Permission tiers — every tool is READ_ONLY, WRITE, or DESTRUCTIVE. Destructive tools are denied by default unless you wire a confirmation hook. Safe by default, not by discipline.

  • Audit log — every invocation (success, error, timeout, and denial) is recorded to SQLite with duration and automatically-redacted sensitive arguments.

  • Argument validation — malformed calls are rejected with a clear InvalidArguments error before they reach a confirmation prompt or your function.

Quickstart

from relay_mcp import RelayServer, PermissionTier

relay = RelayServer("git-tools", audit_path="audit.db")


@relay.tool()
def git_status(repo: str) -> str:
    """Show working tree status.

    Args:
        repo: Absolute path to the git repository.
    """
    ...


@relay.tool(permission=PermissionTier.DESTRUCTIVE)
def delete_branch(repo: str, branch: str) -> str:
    """Force-delete a branch. Irreversible if unmerged."""
    ...


relay.run()  # serves over stdio

That's a complete MCP server. Register it in Claude Desktop / any MCP client:

{
  "mcpServers": {
    "git-tools": { "command": "python", "args": ["git_server.py"] }
  }
}

Related MCP server: mcpnew

Permission model

Tier

Behavior

READ_ONLY

Always allowed. Advertised to clients via MCP readOnlyHint.

WRITE

Allowed unless the server runs with read_only=True.

DESTRUCTIVE

Requires a confirm_hook; denied if none is configured. Advertised via destructiveHint.

def confirm(tool_name: str, arguments: dict) -> bool:
    # prompt a human, check a policy service, whatever you need
    return input(f"Allow {tool_name}({arguments})? [y/N] ") == "y"


relay = RelayServer("ops", confirm_hook=confirm)

Hooks can be sync or async. Sync hooks run in a worker thread, so a terminal prompt waiting on a human never stalls other in-flight tool calls. A rejected or missing confirmation raises PermissionDenied — the MCP client receives it with isError: true, and the denial itself is audited.

For headless or Windows deployments where no terminal is available, use the built-in allowlist hook:

from relay_mcp import allowlist_confirm

relay = RelayServer("ops", confirm_hook=allowlist_confirm("rotate_logs"))

Argument validation

Before the permission check, every call is checked against the tool's generated schema: missing required parameters or unexpected ones raise InvalidArguments (audited as invalid). A destructive call with a broken payload never triggers a confirmation prompt, and your function never sees a TypeError from a bad **kwargs expansion. Tools that declare **kwargs accept extra keys.

Timeouts

A runaway tool can't hang the server:

@relay.tool(timeout=30)  # seconds
def run_build(project: str) -> str: ...

Exceeding the limit raises TimeoutError and records a timeout audit entry.

Audit log

for e in relay.audit.entries(status="denied"):
    print(e.ts, e.tool, e.detail)

Arguments whose names look sensitive (password, token, api_key, ...) are redacted before persistence — recursively, so a secret nested inside a config dict or a list of objects is masked too. The log won't grow forever:

relay.audit.prune(keep_last=10_000)  # or older_than_days=30

Architecture

 agent / IDE (MCP client)
        │  stdio (JSON-RPC)
        ▼
 ┌─────────────────────────────┐
 │  RelayServer                │
 │  ┌───────────┐              │
 │  │ MCP layer │ list_tools / call_tool (official mcp SDK)
 │  └─────┬─────┘              │
 │        ▼                    │
 │  validate_arguments ─invalid┼──► AuditLog (sqlite)
 │        ▼                    │         ▲
 │  PermissionPolicy ──denied──┼─────────┤
 │        ▼                    │         │
 │  execute (thread/async) ────┼──ok/err─┘
 └─────────────────────────────┘

Design rationale

  • Deny destructive by default. Most MCP frameworks trust the client to gate dangerous actions. Relay assumes the agent will eventually call delete_branch with the wrong argument; a missing hook fails closed rather than open.

  • Tiers, not per-tool allowlists. Three tiers are coarse on purpose: an author can classify a tool correctly in one second, which means the classification actually happens. Fine-grained policy belongs in the confirm_hook, where it can be as elaborate as you like.

  • Validate before you ask. Argument validation runs before the permission check so a human is never asked to approve a call that would have crashed anyway.

  • Sync tools run in a thread (asyncio.to_thread) so a slow subprocess call can't block the event loop and stall the transport.

  • Denials are audited, not just failures. In security tooling, "what did the agent try to do" matters as much as what it did.

  • The execute pipeline is transport-independent, so the full permission/audit path is unit-testable without spawning a subprocess.

Examples

Development

pip install -e ".[dev]"
pytest                      # 32 tests, no subprocess or MCP client needed
ruff check . && ruff format --check .

CI runs the suite on Linux, macOS, and Windows across Python 3.10–3.13. Releases publish to PyPI via trusted publishing when a GitHub release is tagged vX.Y.Z.

License

MIT

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    B
    maintenance
    Enables developers to scaffold secure-by-default MCP servers with deny-all permissions, typed tool stubs, and pre-wired audit hooks, while enforcing security posture through CI.
    4 npm
    MIT
  • A
    license
    Not graded
    quality
    A
    maintenance
    Enables developers to expose a single set of tool definitions as MCP server tools over stdio or Streamable HTTP, so Claude Code, Codex, Claude Desktop and Cursor can call them with risk-based annotations, human-approved irreversible writes, typed structured results and long-running job handling built in. It also ships the same tools as a CLI from one source, keeping both surfaces in parity.
    466 npm
    Apache 2.0