Skip to content

mcp tool

Kadyapam edited this page Jun 2, 2026 · 1 revision

McpTool — MCP (Model Context Protocol) bridge

McpTool (tool kind mcp) bridges playbook steps to MCP servers using JSON-RPC 2.0 over HTTP, with optional SSE response parsing. It matches the Python executor's mcp tool kind surface (noetl/tools/mcp/executor.py).

Source: src/tools/mcp.rs

Added in 2.16.0 (noetl/tools#13, tracking noetl/ai-meta#39).


Playbook config shape

tool:
  kind: mcp
  endpoint: "http://localhost:8080/mcp"  # direct endpoint (preferred)
  # — OR — env-var resolution:
  server: kubernetes                      # slug → NOETL_MCP_KUBERNETES_ENDPOINT

  method: tools/call                      # see Method table below (default: tools/call)
  tool: get_pods                          # required for tools/call
  arguments:                             # required for tools/call
    namespace: default
  params: {}                             # passthrough for other JSON-RPC methods

  timeout: 30                            # seconds; see Timeout resolution below
  request_id: 1                          # JSON-RPC request id (default: 1)
  protocol_version: "2025-03-26"         # MCP initialize protocolVersion
  client_name: "noetl-worker"            # clientInfo.name (default: "noetl-worker")
  client_version: "0"                    # clientInfo.version
  capabilities: {}                       # capabilities sent in initialize

Endpoint resolution

Resolution tries these sources in order (first non-empty wins):

Priority Source
1 config.endpoint (or aliases url, server_url, base_url)
2 NOETL_MCP_<SERVER>_ENDPOINT env var, where <SERVER> is config.server uppercased with non-alphanumeric replaced by _
3 NOETL_MCP_URL generic fallback env var

Example env var names:

config.server Env var looked up
kubernetes NOETL_MCP_KUBERNETES_ENDPOINT
my-server NOETL_MCP_MY_SERVER_ENDPOINT
my.server.v2 NOETL_MCP_MY_SERVER_V2_ENDPOINT

Trailing slashes on the resolved endpoint are stripped before use.


Session lifecycle

Each tool invocation:

  1. POSTs an initialize JSON-RPC request to the endpoint.
  2. Captures the Mcp-Session-Id response header if present.
  3. Sends the method-specific JSON-RPC request, attaching the session id header when available.
  4. Servers that do not return a session id are treated as stateless; the tool continues without error.

Session state does not persist across playbook steps — each step creates its own session, consistent with the NoETL execution model (no resident state between blocks).


Methods

Method What it does Required config fields
tools/call (default) Call a named MCP tool with arguments tool, arguments
tools/list List tools advertised by the server
health GET <endpoint>/healthz; no JSON-RPC handshake
(anything else) Send that JSON-RPC method with params as-is params (optional)

For health, the health URL is derived from the endpoint:

  • /mcp, /sse, /message paths are replaced with /healthz.
  • All other paths get /healthz appended.
  • Example: http://host:8080/mcphttp://host:8080/healthz.

Timeout resolution

config.timeout
  → NOETL_MCP_REQUEST_TIMEOUT_SECONDS env var  (default: 60s)
  → 60s hardcoded default
  ─ clamped to NOETL_WORKER_COMMAND_TIMEOUT_SECONDS (default: 180s)

The effective timeout is the minimum of the configured value and the worker command budget. This prevents a single MCP step from consuming the full worker time slice.


SSE response parsing

MCP servers may respond with text/event-stream-shaped bodies (lines starting with data:). The tool handles both shapes transparently:

  1. Attempt a direct JSON parse of the full body.
  2. If that fails, extract data: lines, concatenate their payloads, then JSON-parse the result.
  3. If neither succeeds, return a parse error with a 360-character preview.

Return shape

Success

{
  "status": "ok",
  "server": "kubernetes",
  "endpoint": "http://localhost:8080/mcp",
  "method": "tools/call",
  "tool": "get_pods",
  "arguments": { "namespace": "default" },
  "text": "pod/web-7f9d8b ...",
  "result": { "content": [ { "type": "text", "text": "pod/web-7f9d8b ..." } ] },
  "initialize": { ... }
}

Error

{
  "status": "error",
  "server": "kubernetes",
  "endpoint": "http://localhost:8080/mcp",
  "method": "tools/call",
  "error": "mcp endpoint is required for server 'kubernetes'...",
  "text": "mcp endpoint is required for server 'kubernetes'..."
}

Errors are returned as status: "error" rather than propagating as a tool-level failure, matching the Python tool's shape so playbooks that branch on status work with either worker runtime.

The text field extracts human-readable text from result.content items with type: "text"; falls back to compact JSON serialisation of the full result.


Observability

  • Tracing span mcp.op with attributes method, server, execution_id.
  • health and passthrough requests log at DEBUG (no INFO flood per agents/rules/logging.md).
  • Failures emit a WARN with method, server, endpoint, error, execution_id.

Parity with the Python tool

Surface Python (executor.py) Rust (mcp.rs)
Endpoint resolution config.endpointNOETL_MCP_<SERVER>_URLNOETL_MCP_URL config.endpointNOETL_MCP_<SERVER>_ENDPOINTNOETL_MCP_URL
Session lifecycle POST initialize → Mcp-Session-Id same
Timeout chain config.timeout_secondsNOETL_MCP_REQUEST_TIMEOUT_SECONDS → 60s, clamped config.timeout → same env → 60s, clamped
SSE parsing _parse_mcp_envelope parse_mcp_envelope
Text extraction _extract_text walks content[].type=="text" extract_text — same logic
Return fields status, server, endpoint, method, tool?, arguments?, text, result, initialize, error? same
health method GET <endpoint>/healthz with path normalisation same

Difference: the Python tool also accepts config.timeout_seconds as the key; the Rust tool uses config.timeout to match YAML ergonomics in the Rust playbook runner. Both env vars are the same.


Worked example

steps:
  - id: list_k8s_tools
    tool:
      kind: mcp
      server: kubernetes       # resolves from NOETL_MCP_KUBERNETES_ENDPOINT
      method: tools/list
      timeout: 15

  - id: get_pods
    tool:
      kind: mcp
      server: kubernetes
      method: tools/call
      tool: get_pods
      arguments:
        namespace: "{{ vars.namespace }}"
      timeout: 30

  - id: check_health
    tool:
      kind: mcp
      server: kubernetes
      method: health

Related