Jidoka: MCP Tool Sync
MCP endpoints are tool sources. Jidoka registers endpoints, prefixes synced tools, and keeps sync failures inside the turn boundary.
Setup
Mix.install(
[
{:jidoka, git: "https://github.com/mikehostetler/jidoka.git", ref: "924a486f3c1b7e7a943cb3d5ceee0de65f158467"},
{:kino, "~> 0.19.0"}
],
config: [
jidoka: [
model_aliases: %{fast: "anthropic:claude-haiku-4-5"}
]
]
)
Jidoka.Kino.setup()
Define An Inline MCP Endpoint
defmodule LivebookDemo.MCP.Agent do
use Jidoka.Agent
agent do
id :livebook_mcp_agent
end
defaults do
model :fast
instructions "You can use synced MCP tools."
end
capabilities do
mcp_tools endpoint: :livebook_inline_mcp,
prefix: "demo_",
transport: {:stdio, command: "echo"},
client_info: %{name: "jidoka-livebook", version: "0.1.0"},
timeouts: %{request_ms: 15_000}
end
end
LivebookDemo.MCP.Agent.mcp_tools()
Fake A Successful Sync
This notebook uses a fake sync module so the shape is testable without starting an external MCP server.
defmodule LivebookDemo.MCP.FakeSync do
def run(params, _context) do
send(self(), {:mcp_sync_called, params})
{:ok, %{registered_count: 2, registered_tools: ["demo_read_file", "demo_list_files"]}}
end
end
previous_sync = Application.get_env(:jidoka, :mcp_sync_module)
Application.put_env(:jidoka, :mcp_sync_module, LivebookDemo.MCP.FakeSync)
try do
runtime = LivebookDemo.MCP.Agent.runtime_module()
agent = runtime.new(id: "livebook-mcp-runtime")
{:ok, _agent, {:ai_react_start, _params}} =
Jidoka.MCP.on_before_cmd(agent, {:ai_react_start, %{tool_context: %{}}}, LivebookDemo.MCP.Agent.mcp_tools())
receive do
{:mcp_sync_called, params} ->
Map.take(params, [:endpoint_id, :prefix, :replace_existing])
after
100 -> :sync_not_called
end
after
if previous_sync do
Application.put_env(:jidoka, :mcp_sync_module, previous_sync)
else
Application.delete_env(:jidoka, :mcp_sync_module)
end
end
Keep Failures Bounded
defmodule LivebookDemo.MCP.FailingSync do
def run(params, _context) do
send(self(), {:mcp_sync_called, params})
{:error, :server_capabilities_not_set}
end
end
previous_sync = Application.get_env(:jidoka, :mcp_sync_module)
Application.put_env(:jidoka, :mcp_sync_module, LivebookDemo.MCP.FailingSync)
try do
runtime = LivebookDemo.MCP.Agent.runtime_module()
agent = runtime.new(id: "livebook-mcp-failure-runtime")
{:ok, updated_agent, {:ai_react_start, %{tool_context: context}}} =
Jidoka.MCP.on_before_cmd(agent, {:ai_react_start, %{tool_context: %{}}}, LivebookDemo.MCP.Agent.mcp_tools())
error =
updated_agent.state
|> get_in([:__jidoka_mcp__, :last_errors])
|> List.first()
%{
turn_continued?: context == %{},
formatted_error: error && error.message
}
after
if previous_sync do
Application.put_env(:jidoka, :mcp_sync_module, previous_sync)
else
Application.delete_env(:jidoka, :mcp_sync_module)
end
end
Optional Provider Turn
{:ok, pid} =
Jidoka.Kino.start_or_reuse("livebook-mcp-agent", fn ->
LivebookDemo.MCP.Agent.start_link(id: "livebook-mcp-agent")
end)
Jidoka.Kino.chat("MCP-aware chat", fn ->
LivebookDemo.MCP.Agent.chat(pid, "Explain MCP sync in one sentence.")
end)