Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 68 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,74 @@ adapter = Adapter.new(
)
```

### Client Types

The `client` and `fallback_client` options accept multiple reference types:

```elixir
# Module name (most common - for supervision tree clients)
adapter = Adapter.new(client: MyApp.MCPClient)

# PID (for dynamically started clients)
{:ok, client_pid} = MyApp.MCPClient.start_link(transport: {:streamable_http, base_url: url})
adapter = Adapter.new(client: client_pid)

# Via tuple (for Registry-based lookups)
adapter = Adapter.new(client: {:via, Registry, {MyApp.Registry, "mcp_client"}})

# Global tuple
adapter = Adapter.new(client: {:global, :my_mcp_client})
```

## Dynamic Clients

For scenarios where you need per-request or per-job MCP clients (e.g., browser automation with Playwright), you can start clients dynamically and pass the PID to the adapter.

### Per-Job Pattern

```elixir
defmodule MyApp.BrowserJob do
alias LangChain.MCP.Adapter

def run_with_browser(task) do
# Start a dedicated MCP client for this job
{:ok, client_pid} = MyApp.PlaywrightMCP.start_link(
transport: {:streamable_http, base_url: "http://localhost:3000"}
)

# Wait for the client to be ready
:ok = Adapter.wait_for_server_ready(client_pid)

try do
# Create adapter with the dynamic client
adapter = Adapter.new(client: client_pid)
functions = Adapter.to_functions(adapter)

# Use in your chain
{:ok, result} = run_chain_with_tools(task, functions)
result
after
# Clean up when done
Supervisor.stop(client_pid)
end
end
end
```

### With Fallback

Dynamic clients also work with fallback support:

```elixir
{:ok, primary_pid} = MyApp.PrimaryMCP.start_link(opts)
{:ok, fallback_pid} = MyApp.BackupMCP.start_link(opts)

adapter = Adapter.new(
client: primary_pid,
fallback_client: fallback_pid
)
```

### Selective Tool Discovery

```elixir
Expand Down
71 changes: 67 additions & 4 deletions lib/langchain_mcp/config.ex
Original file line number Diff line number Diff line change
Expand Up @@ -55,12 +55,15 @@ defmodule LangChain.MCP.Config do
field(:context, :map, virtual: true, default: %{})
end

@typedoc "A client reference that can be a module, PID, or GenServer-compatible name"
@type client_ref :: module() | pid() | GenServer.server()

@type t :: %__MODULE__{
client: module(),
client: client_ref(),
cache_tools: boolean(),
timeout: pos_integer(),
async: boolean(),
fallback_client: module() | nil,
fallback_client: client_ref() | nil,
before_fallback: function() | nil,
tool_filter: function() | nil,
context: map()
Expand All @@ -71,15 +74,23 @@ defmodule LangChain.MCP.Config do

## Options

* `:client` - Required. The Anubis.Client module
* `:client` - Required. The Anubis.Client module, PID, or via tuple
* `:cache_tools` - Boolean, default true
* `:timeout` - Positive integer in ms, default 30_000
* `:async` - Boolean, default false
* `:fallback_client` - Optional module
* `:fallback_client` - Optional module, PID, or via tuple
* `:before_fallback` - Optional 3-arity function
* `:tool_filter` - Optional 1-arity function
* `:context` - Optional map

## Client Types

The `:client` and `:fallback_client` options accept:
* Module name (atom) - e.g., `MyApp.MCPClient`
* PID - e.g., a dynamically started client
* Via tuple - e.g., `{:via, Registry, {MyRegistry, "key"}}`
* Global tuple - e.g., `{:global, :my_client}`

## Examples

iex> Config.new!(client: MyApp.MCPClient)
Expand Down Expand Up @@ -116,6 +127,7 @@ defmodule LangChain.MCP.Config do
|> validate_number(:timeout, greater_than: 0)
|> put_virtual_fields(attrs)
|> validate_client()
|> validate_fallback_client()
|> validate_callbacks()
end

Expand Down Expand Up @@ -146,11 +158,62 @@ defmodule LangChain.MCP.Config do
add_error(changeset, :client, "module does not exist")
end

client when is_pid(client) ->
# Check if PID is alive
if Process.alive?(client) do
changeset
else
add_error(changeset, :client, "PID is not alive")
end

{:via, module, _term} when is_atom(module) ->
# Accept via tuples with structure validation only
changeset

{:global, _name} ->
# Accept global tuples
changeset

_ ->
add_error(changeset, :client, "must be a module name (atom)")
end
end

defp validate_fallback_client(changeset) do
case get_field(changeset, :fallback_client) do
nil ->
# Fallback client is optional
changeset

client when is_atom(client) ->
# Check if module exists
if Code.ensure_loaded?(client) do
changeset
else
add_error(changeset, :fallback_client, "module does not exist")
end

client when is_pid(client) ->
# Check if PID is alive
if Process.alive?(client) do
changeset
else
add_error(changeset, :fallback_client, "PID is not alive")
end

{:via, module, _term} when is_atom(module) ->
# Accept via tuples with structure validation only
changeset

{:global, _name} ->
# Accept global tuples
changeset

_ ->
add_error(changeset, :fallback_client, "must be a module name (atom)")
end
end

defp validate_callbacks(changeset) do
changeset
|> validate_function(:before_fallback, 3)
Expand Down
Loading