# LangChain web search tool with Mux (Python and TypeScript)

> Build a LangChain web search tool with the @tool decorator or tool() helper and create_agent, backed by Mux for Exa, Parallel and Keenable on one key.

Canonical: https://getmux.io/guides/web-search-tool-langchain


A LangChain web search tool is a plain function wrapped with `@tool` in Python or `tool()` in TypeScript, with a schema that tells the model what arguments it takes. This guide builds one on top of Mux, so a single `MUX_API_KEY` gives the agent Exa, Parallel and Keenable with automatic fallback, then hands the tool to `create_agent` / `createAgent`. The code targets LangChain 1.x (Python `langchain` 1.4, TypeScript `langchain` 1.5).

## Setup

You need a Mux API key from the [dashboard](https://getmux.io/dashboard/keys) with some credit, plus a key for your chat model. The examples use Anthropic's Claude; any LangChain chat model with tool calling works the same way.

```bash
# Python
uv add langchain langchain-core langchain-anthropic getmux
# TypeScript
npm install langchain @langchain/core @langchain/anthropic zod getmux

export MUX_API_KEY="mux-..."
export ANTHROPIC_API_KEY="sk-ant-..."
```

Versions used for this guide:

| Package | Python | TypeScript |
|---|---|---|
| `langchain` | 1.4.6 | 1.5.18 |
| `langchain-core` / `@langchain/core` | 1.6.12 | 1.2.18 |
| `getmux` | 0.1.0 | 0.1.0 |
| `pydantic` / `zod` | 2.14 | 4.6 |

In LangChain 1.x the agent constructor is `create_agent` (Python) and `createAgent` (TypeScript). Both replace the older `create_react_agent` from LangGraph prebuilts and the `AgentExecutor` from LangChain 0.x. The `@tool` decorator lives in `langchain_core.tools` and is re-exported from `langchain.tools`; in TypeScript `tool()` lives in `@langchain/core/tools` and is re-exported from `langchain`.

## Define the tool

The schema exposes `query`, an optional `max_results` and an optional `include_domains`. The body calls Mux with an ordered fallback list and returns only title, URL, snippet and publish date, so one search adds a few hundred tokens to the conversation instead of thousands.

```python
# web_search.py
from pydantic import BaseModel, Field
from langchain_core.tools import tool
from getmux import Mux, MuxError

mux = Mux()  # reads MUX_API_KEY


class WebSearchInput(BaseModel):
    """Arguments for the web_search tool."""

    query: str = Field(description="The search query, phrased like a web search")
    max_results: int = Field(default=5, ge=1, le=20, description="How many results to return")
    include_domains: list[str] | None = Field(
        default=None, description="Only return results from these domains, e.g. ['arxiv.org']"
    )


@tool(args_schema=WebSearchInput)
def web_search(query: str, max_results: int = 5, include_domains: list[str] | None = None) -> dict:
    """Search the web for current information. Returns titles, URLs, short
    snippets and publish dates. Use it for anything recent or that needs a source."""
    try:
        res = mux.search(
            query,
            models=["parallel/fast", "exa/auto"],
            max_results=max_results,
            include_domains=include_domains,
        )
    except MuxError as e:
        # Hand the problem back as data so the agent can explain it instead of crashing.
        return {"error": e.code, "message": e.message, "results": []}

    return {
        "model": res.model,
        "results": [
            {
                "title": r.title,
                "url": r.url,
                "snippet": r.snippet,
                "published_date": r.published_date,
            }
            for r in res.results
        ],
    }
```
```ts
// web-search.ts
import { tool } from "@langchain/core/tools";
import { z } from "zod";
import { Mux, MuxError } from "getmux";

const mux = new Mux(); // reads MUX_API_KEY

export const webSearch = tool(
  async ({ query, maxResults, includeDomains }) => {
    try {
      const res = await mux.search({
        query,
        models: ["parallel/fast", "exa/auto"],
        maxResults,
        includeDomains,
      });
      return {
        model: res.model,
        results: res.results.map((r) => ({
          title: r.title,
          url: r.url,
          snippet: r.snippet,
          published_date: r.publishedDate,
        })),
      };
    } catch (e) {
      if (e instanceof MuxError) {
        return { error: e.code, message: e.message, results: [] };
      }
      throw e;
    }
  },
  {
    name: "web_search",
    description:
      "Search the web for current information. Returns titles, URLs, short snippets and publish dates. Use it for anything recent or that needs a source.",
    schema: z.object({
      query: z.string().min(1).max(2000).describe("The search query, phrased like a web search"),
      maxResults: z.number().int().min(1).max(20).default(5).describe("How many results to return"),
      includeDomains: z
        .array(z.string())
        .optional()
        .describe("Only return results from these domains, e.g. ['arxiv.org']"),
    }),
  },
);
```

The docstring (Python) or `description` (TypeScript) is what the model reads when deciding whether to call the tool, so say what the tool is for and when to use it. LangChain serializes a dict or object return value to JSON for the tool message.

`models: ["parallel/fast", "exa/auto"]` tries Parallel first at $0.001 per search and falls back to Exa at $0.007 if Parallel errors or is at capacity. Failed attempts are refunded, so you pay for the one that answered. The `auto` default would add `keenable/pro` as a third fallback; list the models yourself when you want to control the order.

## Create the agent

```python
# agent.py
from langchain.agents import create_agent
from web_search import web_search

agent = create_agent(
    model="anthropic:claude-opus-5-5",
    tools=[web_search],
    system_prompt="You are a research assistant. Use web_search for current facts and cite the URL of every source you use.",
)

result = agent.invoke(
    {"messages": [{"role": "user", "content": "What changed in the EU AI Act implementation timeline this month?"}]}
)
print(result["messages"][-1].content)
```
```ts
// agent.ts
import { createAgent } from "langchain";
import { webSearch } from "./web-search";

const agent = createAgent({
  model: "anthropic:claude-opus-5-5",
  tools: [webSearch],
  systemPrompt:
    "You are a research assistant. Use web_search for current facts and cite the URL of every source you use.",
});

const result = await agent.invoke({
  messages: [{ role: "user", content: "What changed in the EU AI Act implementation timeline this month?" }],
});
console.log(result.messages.at(-1)?.content);
```

`create_agent` runs the full loop: the model decides to call `web_search`, LangChain executes it, appends a tool message, and calls the model again until it answers without a tool call. `result["messages"]` holds the whole transcript, including each tool call's arguments and the JSON your tool returned, which is a convenient place to collect the URLs the agent actually consulted. Pass a model instance instead of the string if you need to set temperature or other options.

## Variations

- **Recency filter.** Add `published_after: str | None` to the schema and pass it to `mux.search`. Dates are `YYYY-MM-DD` and normalized across providers. `published_before` is not applied by Parallel models.
- **Full page text.** `content="full"` returns page text on Exa and joined excerpts on Parallel. It is large, so put it in a separate `read_page` tool rather than the main search tool.
- **Different quality or latency.** `["exa/auto", "parallel/advanced"]` favors quality; `["parallel/turbo", "exa/instant"]` favors speed. Every option is listed on the [models page](https://getmux.io/docs/models).

(Live price table: https://getmux.io/models or GET https://api.getmux.io/v1/models)

## Cost control

Agents search more than you expect. Give each agent its own Mux key with a credit limit so a bad loop stops at a number you chose. This needs a management key (`mux-mgmt-...`), created once in the dashboard:

```python
import os
from getmux import Mux

admin = Mux(api_key=os.environ["MUX_MGMT_KEY"])
secret, key = admin.keys.create("langchain-agent", limit=10, limit_reset="daily")
# Put `secret` in MUX_API_KEY for the agent. It is shown only once.
```
```ts
import { Mux } from "getmux";

const admin = new Mux({ apiKey: process.env.MUX_MGMT_KEY });
const { key } = await admin.keys.create({ name: "langchain-agent", limit: 10, limitReset: "daily" });
// Put `key` in MUX_API_KEY for the agent. It is shown only once.
```

Past the limit, Mux returns `402 key_limit_exceeded` and charges nothing. The limit is checked atomically with the debit, so concurrent agents cannot overshoot it. The dashboard breaks usage down per key. See [managing keys](https://getmux.io/docs/keys).

## Handling 402 and 429

Both `getmux` clients retry `429` and most `5xx` responses twice with backoff and honor `Retry-After`. The ones left for you:

| Error | Meaning | In a LangChain tool |
|---|---|---|
| `402 insufficient_credits` | Account balance is empty | Return an error object; alert someone to [add credits](https://getmux.io/docs/credits). |
| `402 key_limit_exceeded` | This key hit its limit for the period | Return an error object; raise the limit or wait for the reset. |
| `429 rate_limited` | Over 20 requests per second on one key | Retried by the SDK. If it persists, lower parallelism. |
| `429 too_many_concurrent_requests` | Over 50 searches in flight on the account | Retried by the SDK. Queue tool calls across agents. |
| `502 all_providers_failed` | Every model in `models` failed | Not retried. Safe to retry once; nothing was charged. |

The example returns `MuxError` as data rather than raising. In LangChain 1.x an uncaught exception inside a tool stops the run unless you add the tool error middleware, and even then the model only sees a generic message. Returning `{"error": ..., "results": []}` lets the model tell the user that search is unavailable and answer from what it knows. Branch on `e.code` if you want different handling for billing versus transient errors. See the [errors page](https://getmux.io/docs/errors).

## FAQ

### How is this different from LangChain's built-in search tool integrations?

LangChain ships community wrappers for individual search APIs, each with its own argument names, result shape and account. The tool here is a few dozen lines you own, with one response shape and one bill across Exa, Parallel and Keenable, and fallback between them. If you only ever want one provider and already have a contract with them, their native integration is a fine choice.

### Can I use this with LangGraph directly?

Yes. `create_agent` returns a compiled LangGraph graph, and the tool itself is a standard `BaseTool`, so you can pass it to a `ToolNode` in your own graph or bind it to a model with `bind_tools`.

### Does `create_agent` limit how many times the tool is called?

Not by default. Use the `recursion_limit` in the invoke config, or LangChain's tool call limit middleware, to cap steps. For a hard ceiling on money rather than steps, use the per-key credit limit described above.

### Which model should the agent use?

Any chat model with tool calling. The examples use Claude via `langchain-anthropic` / `@langchain/anthropic`; swap the `provider:model` string and install the matching package for OpenAI, Google or others. The tool code does not change.

Get a key and run your first search in the [quickstart](https://getmux.io/docs/quickstart).
