# Jido.MCP

[![Hex.pm](https://img.shields.io/hexpm/v/jido_mcp.svg)](https://hex.pm/packages/jido_mcp)
[![Hex Docs](https://img.shields.io/badge/hex-docs-lightgreen.svg)](https://hexdocs.pm/jido_mcp/)
[![CI](https://github.com/agentjido/jido_mcp/actions/workflows/ci.yml/badge.svg)](https://github.com/agentjido/jido_mcp/actions/workflows/ci.yml)
[![License](https://img.shields.io/hexpm/l/jido_mcp.svg)](https://github.com/agentjido/jido_mcp/blob/main/LICENSE)
[![Website](https://img.shields.io/badge/website-jido.run-0f172a.svg)](https://jido.run)
[![Ecosystem](https://img.shields.io/badge/ecosystem-jido.run-0ea5e9.svg)](https://jido.run/ecosystem)
[![Discord](https://img.shields.io/badge/discord-join-5865F2.svg?logo=discord&logoColor=white)](https://jido.run/discord)

`jido_mcp` integrates MCP servers into the Jido ecosystem using `anubis_mcp` directly.

## Features

- Shared pooled MCP clients per configured endpoint
- Consume-side API for MCP tools/resources/prompts
- Jido actions + plugin routes for signal-driven usage
- Jido.AI runtime tool sync (MCP tools -> proxy `Jido.Action`s)
- MCP server bridge (`use Jido.MCP.Server`) with explicit allowlists

## Installation

```elixir
def deps do
  [
    {:jido_mcp, "~> 0.1"}
  ]
end
```

## Endpoint Configuration

```elixir
config :jido_mcp, :endpoints,
  github: %{
    transport: {:streamable_http, [base_url: "http://localhost:8080", mcp_path: "/mcp"]},
    client_info: %{name: "my_app", version: "1.0.0"},
    protocol_version: "2025-06-18",
    capabilities: %{},
    timeouts: %{request_ms: 30_000}
  },
  local_fs: %{
    transport: {:stdio, [command: "uvx", args: ["mcp-server-filesystem"]]},
    client_info: %{name: "my_app", version: "1.0.0"}
  }
```

Supported transports in v1:

- `{:stdio, keyword()}` for shell/command-based MCP servers
- `{:shell, keyword()}` as an alias normalized to `:stdio`
- `{:sse, keyword()}` for legacy HTTP+SSE servers using protocol `2024-11-05`
- `{:streamable_http, keyword()}` for MCP `2025-03-26` / `2025-06-18`, including optional SSE streaming via Anubis' `:enable_sse` option

For Streamable HTTP, `:url` or a `:base_url` containing a non-root path is
normalized to Anubis' `:base_url` + `:mcp_path` option shape.

Endpoint config may also be loaded through an MFA callback:

```elixir
config :jido_mcp, :endpoints, {MyApp.MCPConfig, :endpoints, []}
```

The callback must return a map/keyword endpoint declaration or `{:ok, endpoints}`.

Runtime endpoints can be registered after application start:

```elixir
{:ok, endpoint} =
  Jido.MCP.Endpoint.new(:github, %{
    transport: {:streamable_http, [base_url: "http://localhost:8080", mcp_path: "/mcp"]},
    client_info: %{name: "my_app", version: "1.0.0"}
  })

{:ok, ^endpoint} = Jido.MCP.register_endpoint(endpoint)
```

Runtime registration is process-local, rejects duplicate endpoint ids, and starts the MCP client
only when the endpoint is first used.

## Runtime Endpoint Lifecycle

```elixir
{:ok, endpoint} =
  Jido.MCP.Endpoint.new(:runtime_demo, %{
    transport: {:streamable_http, [base_url: "http://localhost:8080/mcp"]},
    client_info: %{name: "my_app"}
  })

{:ok, ^endpoint} = Jido.MCP.register_endpoint(endpoint)
{:ok, tools} = Jido.MCP.list_tools(:runtime_demo)

{:ok, _removed} = Jido.MCP.unregister_endpoint(:runtime_demo)
```

For config changes at runtime, unregister then register the updated endpoint.

## Consume MCP APIs

```elixir
{:ok, tools} = Jido.MCP.list_tools(:github)
{:ok, called} = Jido.MCP.call_tool(:github, "search_issues", %{"query" => "label:bug"})

{:ok, resources} = Jido.MCP.list_resources(:github)
{:ok, content} = Jido.MCP.read_resource(:github, "repo://owner/name/README")

{:ok, prompts} = Jido.MCP.list_prompts(:github)
{:ok, prompt} = Jido.MCP.get_prompt(:github, "release_notes", %{"version" => "1.2.0"})
```

All calls return normalized envelopes:

- success: `%{status: :ok, endpoint: atom(), method: String.t(), data: map(), raw: ...}`
- error: `%{status: :error, endpoint: atom(), type: ..., message: String.t(), details: ...}`

## Jido Actions + Plugin

### Actions

- `Jido.MCP.Actions.ListTools`
- `Jido.MCP.Actions.CallTool`
- `Jido.MCP.Actions.ListResources`
- `Jido.MCP.Actions.ListResourceTemplates`
- `Jido.MCP.Actions.ReadResource`
- `Jido.MCP.Actions.ListPrompts`
- `Jido.MCP.Actions.GetPrompt`
- `Jido.MCP.Actions.RegisterEndpoint`
- `Jido.MCP.Actions.RefreshEndpoint`
- `Jido.MCP.Actions.UnregisterEndpoint`
- `Jido.MCP.Actions.SetDefaultEndpoint`

### Plugin

```elixir
defmodule MyApp.Agent do
  use Jido.Agent,
    name: "assistant",
    plugins: [
      {Jido.MCP.Plugins.MCP,
        %{
          default_endpoint: :github,
          allowed_endpoints: [:github, :local_fs]
          # or allowed_endpoints: :all
        }}
    ]
end
```

`allowed_endpoints` defaults to `[]` (deny-all) when omitted.
Set it to `:all` to allow all currently configured/runtime-registered endpoints.

Signal routes:

- `mcp.tools.list`
- `mcp.tools.call`
- `mcp.resources.list`
- `mcp.resources.templates.list`
- `mcp.resources.read`
- `mcp.prompts.list`
- `mcp.prompts.get`
- `mcp.endpoint.register`
- `mcp.endpoint.refresh`
- `mcp.endpoint.unregister`
- `mcp.endpoint.default.set`

To update the plugin default endpoint at runtime, emit `mcp.endpoint.default.set` with
`%{endpoint_id: "github"}` (or `nil`/omitted to clear).

## Jido.AI Sync

`Jido.MCP.JidoAI.Actions.SyncToolsToAgent` discovers remote tools and creates proxy `Jido.Action` modules, then registers them on a running `Jido.AI.Agent`.

`Jido.MCP.JidoAI.Actions.UnsyncToolsFromAgent` removes previously synced proxies.

Tool sync uses deterministic MCP readiness: endpoint calls wait on
`Anubis.Client.await_ready/2` before executing.

Plugin route support:

- `mcp.ai.sync_tools`
- `mcp.ai.unsync_tools`

### Host Runtime Orchestration (Signals)

Host projects are expected to orchestrate runtime endpoint lifecycle and tool sync
explicitly using plugin signals.

Recommended signal sequences:

- Register endpoint then sync tools
  1. `mcp.endpoint.register`
  2. `mcp.ai.sync_tools`
- Refresh endpoint then resync tools
  1. `mcp.endpoint.refresh`
  2. `mcp.ai.sync_tools` (with `replace_existing: true`)
- Unsync tools before unregistering endpoint
  1. `mcp.ai.unsync_tools`
  2. `mcp.endpoint.unregister`

Example signal payloads:

```elixir
# mcp.endpoint.register
%{
  endpoint_id: "runtime_demo",
  endpoint: %{
    transport: {:streamable_http, [base_url: "http://localhost:8080/mcp"]},
    client_info: %{name: "my_app"}
  }
}

# mcp.ai.sync_tools
%{
  endpoint_id: "runtime_demo",
  agent_server: :my_ai_agent,
  replace_existing: true,
  prefix: "mcp_"
}

# mcp.ai.unsync_tools
%{endpoint_id: "runtime_demo", agent_server: :my_ai_agent}

# mcp.endpoint.unregister
%{endpoint_id: "runtime_demo"}
```

## Expose Jido As MCP Server

### Server module

```elixir
defmodule MyApp.MCPServer do
  use Jido.MCP.Server,
    name: "my-app",
    version: "1.0.0",
    publish: %{
      tools: [MyApp.Actions.SearchIssues],
      resources: [MyApp.MCP.Resources.ReleaseNotes],
      prompts: [MyApp.MCP.Prompts.CodeReview]
    }
end
```

Publication is explicit allowlist only.

### Supervision

```elixir
children =
  Jido.MCP.Server.server_children(MyApp.MCPServer,
    transport: :streamable_http
  )
```

### Router (streamable HTTP)

```elixir
forward "/mcp", Anubis.Server.Transport.StreamableHTTP.Plug,
  Jido.MCP.Server.plug_init_opts(MyApp.MCPServer)
```

## Resource and Prompt Behaviours

- `Jido.MCP.Server.Resource`
- `Jido.MCP.Server.Prompt`

Implement those behaviours for items listed in `publish.resources` and `publish.prompts`.

## Testing

```bash
mix test
```
