-
Notifications
You must be signed in to change notification settings - Fork 0
mcp tool
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).
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 initializeResolution 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.
Each tool invocation:
- POSTs an
initializeJSON-RPC request to the endpoint. - Captures the
Mcp-Session-Idresponse header if present. - Sends the method-specific JSON-RPC request, attaching the session id header when available.
- 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).
| 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,/messagepaths are replaced with/healthz. - All other paths get
/healthzappended. - Example:
http://host:8080/mcp→http://host:8080/healthz.
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.
MCP servers may respond with text/event-stream-shaped bodies (lines
starting with data:). The tool handles both shapes transparently:
- Attempt a direct JSON parse of the full body.
- If that fails, extract
data:lines, concatenate their payloads, then JSON-parse the result. - If neither succeeds, return a parse error with a 360-character preview.
{
"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": { ... }
}{
"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.
- Tracing span
mcp.opwith attributesmethod,server,execution_id. -
healthand passthrough requests log at DEBUG (no INFO flood peragents/rules/logging.md). - Failures emit a WARN with
method,server,endpoint,error,execution_id.
| Surface | Python (executor.py) |
Rust (mcp.rs) |
|---|---|---|
| Endpoint resolution |
config.endpoint → NOETL_MCP_<SERVER>_URL → NOETL_MCP_URL
|
config.endpoint → NOETL_MCP_<SERVER>_ENDPOINT → NOETL_MCP_URL
|
| Session lifecycle | POST initialize → Mcp-Session-Id
|
same |
| Timeout chain |
config.timeout_seconds → NOETL_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.
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- noetl/tools wiki — Home — tool registry overview.
- noetl/tools wiki — NatsTool — sibling tool using the same per-tool module pattern.
- Python reference — noetl/tools/mcp/executor.py
- MCP specification — JSON-RPC 2.0 protocol reference.
- NoETL execution model — why sessions don't persist across steps.