> ## Documentation Index
> Fetch the complete documentation index at: https://edenai-auto-update-feature-docs.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# MCP Server

> Eden AI's expert models are available as MCP tools, so any MCP client or LLM agent loop can call OCR, web search, speech, translation and more.

export const TechArticleSchema = ({title, description, path, articleSection, about, proficiencyLevel = "Beginner", dependencies, keywords = [], datePublished, dateModified, image, inLanguage = "en"}) => {
  const baseUrl = "https://www.edenai.co/docs";
  const canonicalUrl = `${baseUrl}/${path}`.replace(/\/+$/, "");
  const ogParams = new URLSearchParams({
    division: articleSection || "",
    title: title || "",
    description: description || ""
  });
  const resolvedImage = image || `https://edenai.mintlify.app/_mintlify/api/og?${ogParams.toString()}`;
  const data = {
    "@context": "https://schema.org",
    "@type": "TechArticle",
    "@id": `${canonicalUrl}#techarticle`,
    mainEntityOfPage: {
      "@type": "WebPage",
      "@id": canonicalUrl
    },
    headline: title,
    name: title,
    description: description,
    url: canonicalUrl,
    inLanguage: inLanguage,
    isPartOf: {
      "@type": "WebSite",
      name: "Eden AI Documentation",
      url: baseUrl
    },
    author: [{
      "@type": "Organization",
      name: "Eden AI",
      url: "https://www.edenai.co/"
    }],
    publisher: {
      "@type": "Organization",
      name: "Eden AI",
      url: "https://www.edenai.co/",
      logo: {
        "@type": "ImageObject",
        url: "https://www.edenai.co/assets/logo.png"
      }
    }
  };
  if (articleSection) data.articleSection = articleSection;
  if (about) data.about = {
    "@type": "Thing",
    name: about
  };
  if (proficiencyLevel) data.proficiencyLevel = proficiencyLevel;
  if (dependencies) data.dependencies = dependencies;
  if (keywords && keywords.length) data.keywords = keywords;
  if (datePublished) data.datePublished = datePublished;
  if (dateModified) data.dateModified = dateModified;
  data.image = Array.isArray(resolvedImage) ? resolvedImage : [resolvedImage];
  const json = JSON.stringify(data);
  const schemaId = `techarticle-${canonicalUrl}`;
  React.useEffect(() => {
    if (typeof document === "undefined") return;
    document.querySelectorAll(`script[data-schema-id="${schemaId}"]`).forEach(n => n.remove());
    const script = document.createElement("script");
    script.type = "application/ld+json";
    script.dataset.schemaId = schemaId;
    script.textContent = json;
    document.head.appendChild(script);
    return () => script.remove();
  }, [json, schemaId]);
  return null;
};

<TechArticleSchema title={"MCP Server"} description={"Eden AI's expert models are available as MCP tools, so any MCP client or LLM agent loop can call OCR, web search, speech, translation and more."} path="v3/expert-models/mcp-server" articleSection="Expert Models" about={"AI API"} proficiencyLevel="Intermediate" keywords={["Eden AI", "AI API", "MCP", "Model Context Protocol", "expert models", "tool calling"]} datePublished="2026-08-27T00:00:00Z" dateModified="2026-08-27T00:00:00Z" />

Eden AI's expert models are exposed as tools on a hosted [Model Context Protocol](https://modelcontextprotocol.io) server. OCR, web search, image analysis, speech, translation and the rest of the catalog become callable tools for any MCP client or agent loop, with no server to run and no per-provider integration work.

Pair the server with any function-calling model on the [AI Gateway](/v3/overview/ai-gateway) and a text-only model becomes multimodal by composition: the model asks for `ocr`, your loop runs it against the MCP server, and the extracted text comes back into the conversation.

## Endpoint

|                    |                                      |
| ------------------ | ------------------------------------ |
| **URL**            | `https://mcp.edenai.run/mcp`         |
| **Transport**      | Streamable HTTP                      |
| **Authentication** | `Authorization: Bearer YOUR_API_KEY` |

<Note>
  Every tool call is a normal Eden AI expert model call: it is billed to the key in the `Authorization` header and appears in your [monitoring](/v3/general/monitoring) dashboard. Rate limits and [data governance](/v3/data-governance/provider-data-policies) rules apply exactly as they do on the REST API.
</Note>

## Connect an MCP Client

Any MCP-capable client can use the server with the three values above, but the config syntax differs from client to client: the key names, the transport label and the place headers go are all client-specific. These integration guides carry a ready-to-copy block for each:

| Client                                                                 | Where the config lives                         |
| ---------------------------------------------------------------------- | ---------------------------------------------- |
| [Claude Code](/v3/integrations/claude-code#expert-models-as-tools-mcp) | `claude mcp add`, or the project's `.mcp.json` |
| [Codex CLI](/v3/integrations/codex-cli#expert-models-as-tools-mcp)     | `~/.codex/config.toml`                         |
| [Cline](/v3/integrations/cline#expert-models-as-tools-mcp)             | `cline_mcp_settings.json`                      |
| [Continue](/v3/integrations/continue-dev#expert-models-as-tools-mcp)   | the assistant's `config.yaml`                  |
| [OpenCode](/v3/integrations/opencode#expert-models-as-tools-mcp)       | `opencode.json`                                |
| [Hermes Agent](/v3/integrations/hermes#expert-models-as-tools-mcp)     | `~/.hermes/config.yaml`                        |
| [OpenClaw](/v3/integrations/openclaw#expert-models-as-tools-mcp)       | `~/.openclaw/openclaw.json`                    |

For a client that is not listed, check its MCP documentation for how it declares a remote streamable-HTTP server with custom headers. If your client cannot consume MCP at all, you can still use the tools by driving the loop yourself: see [Give the Tools to Any LLM](#give-the-tools-to-any-llm).

## Tool Catalog

The server publishes one tool per expert model feature, plus three utility tools. The feature tools track the catalog documented in this section, so the [AI Features Reference](/v3/expert-models/features) is the live list: OCR, image, text, web, translation, audio and video. Tool names follow their feature, for example `ocr`, `web_search`, `image_generation` and `translation_automatic_translation`.

| Utility tool  | Purpose                                                              |
| ------------- | -------------------------------------------------------------------- |
| `upload_file` | Upload local content and get a file ID to pass to any file parameter |
| `check_job`   | Fetch the status and result of a long-running tool's job             |
| `list_models` | List providers, models and pricing, optionally for a single tool     |

Every tool carries a JSON Schema for its parameters, so a client discovers the catalog at runtime rather than hardcoding it. Ask the server what it has:

<CodeGroup>
  ```python Python theme={null}
  async def print_catalog(mcp):
      for tool in (await mcp.list_tools()).tools:
          required = tool.input_schema.get("required", [])
          print(f"{tool.name}({', '.join(required)})")
  ```
</CodeGroup>

### Shared Conventions

Every feature tool follows the same three rules:

* **`model` selects the provider.** Pass `"provider"` or `"provider/model"`, for example `"firecrawl"` or `"amazon"`. Call the `list_models` tool for the exact names, pricing and regions, optionally filtered to one tool with `{"tool": "web_search"}`.
* **File parameters take a URL or a file ID.** If your document already has a public URL, pass it directly. For local content, call `upload_file` first and pass the returned ID.
* **Long-running tools return a job.** Tools for the async features return a job ID immediately instead of a result, and say `Long-running` in their description. Poll with `check_job` until `status` is `success` or `fail`.

## Give the Tools to Any LLM

You do not need an MCP-aware client. Fetch the catalog, hand the schemas to a model as function-calling tools, and execute the calls the model asks for. The loop below uses the OpenAI SDK against the Eden AI gateway:

```bash theme={null}
pip install "mcp>=2" openai
```

<Note>
  These examples require the `mcp` Python SDK 2.0 or later.
</Note>

<CodeGroup>
  ```python Python theme={null}
  import asyncio
  import json
  import os

  from mcp import ClientSession
  from mcp.client.streamable_http import create_mcp_http_client, streamable_http_client
  from openai import AsyncOpenAI

  API_KEY = os.environ["EDENAI_API_KEY"]
  MCP_URL = "https://mcp.edenai.run/mcp"
  MODEL = "google/gemini-flash-latest"
  TOOLS = {"web_search", "ocr"}
  RESULT_CAP = 20_000


  async def main():
      headers = {"Authorization": f"Bearer {API_KEY}"}
      async with (
          create_mcp_http_client(headers=headers) as http_client,
          streamable_http_client(MCP_URL, http_client=http_client) as (read, write),
          ClientSession(read, write) as mcp,
      ):
          await mcp.initialize()

          # 1. Turn the MCP catalog into OpenAI function tools.
          catalog = (await mcp.list_tools()).tools
          tools = [
              {
                  "type": "function",
                  "function": {
                      "name": tool.name,
                      "description": tool.description or "",
                      "parameters": tool.input_schema,
                  },
              }
              for tool in catalog
              if tool.name in TOOLS
          ]

          # 2. Run the model and execute whatever it asks for.
          client = AsyncOpenAI(base_url="https://api.edenai.run/v3", api_key=API_KEY)
          messages = [{"role": "user", "content": "What is the latest stable release of Python?"}]

          for _ in range(6):
              response = await client.chat.completions.create(
                  model=MODEL, messages=messages, tools=tools
              )
              message = response.choices[0].message

              if not message.tool_calls:
                  print(message.content)
                  break

              # Rebuild the tool calls from spec fields only (see Best Practices).
              messages.append({
                  "role": "assistant",
                  "content": message.content or "",
                  "tool_calls": [
                      {
                          "id": call.id,
                          "type": "function",
                          "function": {
                              "name": call.function.name,
                              "arguments": call.function.arguments,
                          },
                      }
                      for call in message.tool_calls
                  ],
              })

              for call in message.tool_calls:
                  result = await mcp.call_tool(
                      call.function.name, json.loads(call.function.arguments or "{}")
                  )
                  text = "\n".join(
                      block.text for block in result.content if block.type == "text"
                  )
                  if result.is_error:
                      text = f"Tool error: {text}"
                  messages.append({
                      "role": "tool",
                      "tool_call_id": call.id,
                      "content": text[:RESULT_CAP],
                  })


  asyncio.run(main())
  ```
</CodeGroup>

The model receives the expert model output as an ordinary tool result and grounds its answer in it. Nothing about the loop is Eden-specific beyond the base URL and the key, so the same code works with any MCP server you already use.

## Anthropic SDK

The same pattern works on the gateway's Anthropic-compatible surface, and the model does not have to be a Claude model. Any function-calling model in the catalog can be driven through this SDK.

<Warning>
  On this surface, a custom tool named exactly `web_search` collides with the provider-native web search feature on some models and the request fails. Register the tool under a different name and map it back when you execute the call, as shown below.
</Warning>

```bash theme={null}
pip install "mcp>=2" anthropic
```

<CodeGroup>
  ```python Python theme={null}
  import asyncio
  import os

  from anthropic import AsyncAnthropic
  from mcp import ClientSession
  from mcp.client.streamable_http import create_mcp_http_client, streamable_http_client

  API_KEY = os.environ["EDENAI_API_KEY"]
  MCP_URL = "https://mcp.edenai.run/mcp"
  MODEL = "anthropic/claude-haiku-latest"
  RESULT_CAP = 20_000

  # Exposed name -> MCP tool name. Avoids the web_search collision.
  ALIASES = {"internet_search": "web_search"}


  async def main():
      headers = {"Authorization": f"Bearer {API_KEY}"}
      async with (
          create_mcp_http_client(headers=headers) as http_client,
          streamable_http_client(MCP_URL, http_client=http_client) as (read, write),
          ClientSession(read, write) as mcp,
      ):
          await mcp.initialize()

          catalog = {tool.name: tool for tool in (await mcp.list_tools()).tools}
          tools = [
              {
                  "name": alias,
                  "description": catalog[name].description or "",
                  "input_schema": catalog[name].input_schema,
              }
              for alias, name in ALIASES.items()
          ]

          client = AsyncAnthropic(base_url="https://api.edenai.run/v3", auth_token=API_KEY)
          messages = [{"role": "user", "content": "What is the latest stable release of Python?"}]

          for _ in range(6):
              response = await client.messages.create(
                  model=MODEL, max_tokens=2048, messages=messages, tools=tools
              )
              tool_uses = [b for b in response.content if b.type == "tool_use"]
              texts = [b.text for b in response.content if b.type == "text"]

              if not tool_uses:
                  print("".join(texts))
                  break

              messages.append({"role": "assistant", "content": [
                  *({"type": "text", "text": t} for t in texts if t),
                  *({"type": "tool_use", "id": b.id, "name": b.name, "input": b.input}
                    for b in tool_uses),
              ]})

              results = []
              for block in tool_uses:
                  result = await mcp.call_tool(ALIASES[block.name], block.input)
                  text = "\n".join(
                      part.text for part in result.content if part.type == "text"
                  )
                  if result.is_error:
                      text = f"Tool error: {text}"
                  results.append({
                      "type": "tool_result",
                      "tool_use_id": block.id,
                      "content": text[:RESULT_CAP],
                  })
              messages.append({"role": "user", "content": results})


  asyncio.run(main())
  ```
</CodeGroup>

## Working With Documents

File parameters accept a public URL directly, so an agent that finds a PDF on the web can pass the link straight to `ocr` with no upload step.

For local files, call `upload_file` once and reuse the returned ID across as many tools as you like:

<CodeGroup>
  ```python Python theme={null}
  import base64
  import json
  from pathlib import Path


  async def read_document(mcp, path):
      path = Path(path)
      upload = await mcp.call_tool("upload_file", {
          "content_base64": base64.b64encode(path.read_bytes()).decode(),
          "filename": path.name,
          "expires_in_days": 7,
      })
      file_id = json.loads(upload.content[0].text)["file_id"]

      result = await mcp.call_tool("ocr", {
          "model": "amazon",
          "file": file_id,
          "language": "en",
      })
      return json.loads(result.content[0].text)
  ```
</CodeGroup>

The tool sends a 30-day retention by default and accepts 1 to 30 days in `expires_in_days`. Uploading through [the REST API](/v3/llms/file-upload) instead keeps a file for 7 days unless you ask for longer.

<Tip>
  When you mention a file ID in a prompt so the model can pass it to a tool itself, put it in a fenced code block. Models occasionally mistranscribe a UUID embedded in plain prose, which produces a confusing 404 from the tool.
</Tip>

### Polling Long-Running Tools

Async tools hand back a job ID. Poll it with `check_job`:

<CodeGroup>
  ```python Python theme={null}
  import asyncio
  import json


  async def ocr_pdf(mcp, file_id, timeout_s=300, interval_s=3):
      job = await mcp.call_tool("ocr_async", {"model": "amazon", "file": file_id})
      job_id = json.loads(job.content[0].text)["job_id"]
      deadline = asyncio.get_running_loop().time() + timeout_s

      while True:
          status = await mcp.call_tool("check_job", {"job_id": job_id})
          payload = json.loads(status.content[0].text)
          if payload["status"] in ("success", "fail"):
              return payload
          remaining = deadline - asyncio.get_running_loop().time()
          if remaining <= 0:
              raise TimeoutError(f"job {job_id} did not finish within {timeout_s}s")
          await asyncio.sleep(min(interval_s, remaining))
  ```
</CodeGroup>

When you expose `check_job` to a model alongside an async tool, the model handles this polling on its own.

## Which Models Can Call the Tools

Any model whose `capabilities.supports_function_calling` is `true` can use the MCP tools. Filter the [model catalog](/v3/llms/listing-models) to find them:

<CodeGroup>
  ```python Python theme={null}
  import requests

  response = requests.get("https://api.edenai.run/v3/models")

  tool_models = [
      model["id"]
      for model in response.json()["data"]
      if (model.get("capabilities") or {}).get("supports_function_calling")
  ]
  print(len(tool_models), "models support tool calling")
  ```
</CodeGroup>

<Tip>
  You can also browse models visually in the [Eden AI model catalog](https://app.edenai.run/models) and filter by capability.
</Tip>

Smaller models sometimes announce a tool call in prose instead of emitting one. If a model narrates ("I will search the web for that") without producing a tool call, a more capable model usually fixes it.

## Best Practices

* **Echo back only the fields in the spec.** Gateway responses can carry non-standard extras on `tool_calls`, such as an `index` field. Sending those back on the next turn is rejected by strict providers. Rebuild minimal `{id, type, function: {name, arguments}}` objects, and use `""` rather than `null` for the assistant message content.
* **Cap the size of tool results.** OCR and scraping output can be very large. Truncating to roughly 20,000 characters before appending to the conversation keeps the context manageable without losing the useful part.
* **Treat tool errors as results, not exceptions.** A failed tool call comes back with `is_error` set rather than raising. Pass the error text to the model and it will usually retry with corrected arguments.
* **Filter the catalog before sending it.** The full catalog is a lot of schema for one prompt. Send only the tools the task needs, which improves both accuracy and cost.
* **Use URLs when you have them.** Skipping `upload_file` removes a round trip and a class of transcription errors.

## Next Steps

<CardGroup cols={2}>
  <Card title="OCR" icon="file-lines" href="/v3/expert-models/features/ocr/ocr">
    Extract text from documents and images
  </Card>

  <Card title="Web Search" icon="magnifying-glass" href="/v3/expert-models/features/web/search">
    Search the web and get ranked results
  </Card>

  <Card title="List Expert Models" icon="list" href="/v3/expert-models/listing-models">
    Browse every provider and model available
  </Card>

  <Card title="Monitoring" icon="chart-line" href="/v3/general/monitoring">
    Track the cost of your tool calls
  </Card>
</CardGroup>
