Build MCP Servers: Using Gradio

Table of Contents

Previous posts looked at MCP Python SDK, FastMCP v2 and FastAPI-MCP for building MCP servers. There is one more option I would like us to explore. And that is Gradio. The official Gradio description “open-source Python package that allows you to quickly build a demo or web application…” isn’t screaming MCP. But I’ll try explain why it’s worth taking a note.

As for other posts in the series, you might find it helpful to be familiar with the MCP Inspector and the general idea behind what and why MCP is in my MCP Introduction.

Install

pip install gradio[mcp]

Simple MCP Server

With Gradio too you would usually start with a function you want to expose as an MCP tool. But because Gradio is more UI focused, it asks you to define the exact input elements to use:

# server.py
import gradio as gr


def add_numbers(a: int, b: int) -> int:
    """Add two numbers"""
    return a + b


demo = gr.Interface(
    fn=add_numbers,
    api_name="add_numbers",
    inputs=[
        gr.Number(precision=0, value=1),
        gr.Number(precision=0, value=2)
    ],
    outputs=[gr.Number(precision=0)],
)


if __name__ == "__main__":
    demo.launch(mcp_server=True)

We can then run the application:

python server.py

The Gradio App will be available under: http://127.0.0.1:7860

The MCP server will be available with the two transport modes:

  • Streamable HTTP: http://127.0.0.1:7860/gradio_api/mcp/http/ (since v5.32.0)
  • SSE (deprecated): http://127.0.0.1:7860/gradio_api/mcp/sse

In the example code, the api_name is used to name the tool (the default would be predict). Inputs need to be specified and are not inferred from the type hints. Using precision=0 forces it to use integer. The description is extracted from the docstring of the function.

List and Run Tools

In the MCP Inspector this should look familar. Under Tools click on List Tools and you should see the following tool listed:

  • add_numbers

When you click on it, you can run it:

MCP Inspector show result of add_numbers tool

Tools schema generated by Gradio

The tools schema generated by Gradio is similar to using other libraries. However, it uses number instead of integer.

{
  "tools": [
    {
      "name": "add_numbers",
      "description": "Add two numbers",
      "inputSchema": {
        "type": "object",
        "properties": {
          "a": {
            "type": "number",
            "default": 1
          },
          "b": {
            "type": "number",
            "default": 2
          }
        }
      }
    }
  ]
}

Gradio UI for add_numbers

This is what you would see in the Gradio UI (http://127.0.0.1:7860):

Gradio UI showing result of add_numbers tool

Gradio Input Types

Let’s test some more input types and how they are represented in the tools schema:

    inputs=[
        gr.Number(label="Number (precision=0)", precision=0),
        gr.Number(label="Number (precision=1)", precision=1),
        gr.Slider(minimum=0, maximum=10, value=5),
        gr.Textbox(),
        gr.Checkbox(value=True),
        gr.Radio(choices=["one", "two", "three"]),
        gr.Dropdown(choices=["one", "two", "three"]),
        gr.DateTime(
            label="DateTime (/wo time)", include_time=False
        ),
        gr.DateTime(
            label="DateTime (/w time)", include_time=True
        )
    ],

This is how it looks like in the Gradio UI:

Gradio UI showing various input types

Below are the types and the generated tools schema:

Number (precision=0)

gr.Number(label="Number (precision=0)", precision=0)

Tools schema:

{
  "type": "number"
}

This should ideally be integer type.

Number (precision=1)

gr.Number(label="Number (precision=1)", precision=1)

Tools schema:

{
  "type": "number"
}

Slider

gr.Slider(minimum=0, maximum=10, value=5)

Tools schema:

{
  "type": "number",
  "description": "numeric value between 0 and 10",
  "default": 5
}

Still not an integer. The range is part of the description but could be expressed in the schema.

Textbox

gr.Textbox()

Tools schema:

{
  "type": "string"
}

Checkbox

gr.Checkbox(value=True)

Tools schema:

{
  "type": "boolean",
  "default": true
}

Radio

gr.Radio(choices=["one", "two", "three"])

Tools schema:

{
  "enum": [
    "one",
    "two",
    "three"
  ],
  "title": "Radio",
  "type": "string"
}

Great that it expresses the values as enum.

gr.Dropdown(choices=["one", "two", "three"])

Tools schema:

{
  "type": "string",
  "enum": [
    "one",
    "two",
    "three"
  ],
  "default": "one"
}

DateTime without time

gr.DateTime(label="DateTime (/wo time)", include_time=False)

Tools schema:

{
  "type": "string",
  "description": "Formatted as YYYY-MM-DD"
}

DateTime with time

gr.DateTime(label="DateTime (/w time)", include_time=True)

Tools schema:

{
  "type": "string",
  "description": "Formatted as YYYY-MM-DD HH:MM:SS"
}

Monkey Patch Input API Info

The tools schema is generated from the api_info provided by the Gradio component. For the Number class this looks like this:

    def api_info(self) -> dict[str, str]:
        return {"type": "number"}

We could just extend Number and return a different api_info:

class Integer(gr.Number):
    def api_info(self) -> dict[str, str]:
        return {"type": "integer"}

That actually works, but the Gradio UI would be broken.

An alternative is to just monkey patch the api_info function:

def get_integer(*args, precision: int = 0, **kwargs) -> gr.Number:
    assert precision == 0
    number = gr.Number(*args, precision=precision, **kwargs)
    number.api_info = (  # type: ignore[method-assign]
        lambda: {"type": "integer"}
    )
    return number

...
    inputs=[
        get_integer(),
    ],

This sure is ugly.

The proper way would be to create a Custom Component.

Multiple Tools

You can also provide multiple tools by defining multiple interfaces that are then combined using TabbedInterface:

import gradio as gr


def add_numbers(a: int, b: int) -> int:
    """Add two numbers"""
    return a + b


def multiply_numbers(a: int, b: int) -> int:
    """Multiply two numbers"""
    return a * b


add_numbers_interface = gr.Interface(
    fn=add_numbers,
    api_name="add_numbers",
    inputs=[
        gr.Number(precision=0, value=1),
        gr.Number(precision=0, value=2)
    ],
    outputs=[gr.Number(precision=0)],
)

multiply_numbers_interface = gr.Interface(
    fn=multiply_numbers,
    api_name="multiply_numbers",
    inputs=[
        gr.Number(precision=0, value=1),
        gr.Number(precision=0, value=2)
    ],
    outputs=[gr.Number(precision=0)],
)


demo = gr.TabbedInterface(
    interface_list=[
        add_numbers_interface,
        multiply_numbers_interface
    ],
    tab_names=["Add", "Multiply"]
)


if __name__ == "__main__":
    demo.launch(mcp_server=True)

That would then look like this:

Gradio UI showing tabbed interface

Both tools are listed as MCP tools:

MCP Inspector showing multiple tools

Mounting GradioMCPServer into FastAPI app

To have more control, you could also mount a GradioMCPServer into an existing FastAPI app:

from fastapi import FastAPI
import gradio as gr
from gradio.mcp import GradioMCPServer

demo = gr.Interface(...)

mcp_server = GradioMCPServer(demo)

app = FastAPI(lifespan=mcp_server.lifespan)
app.mount("/mcp", app=mcp_server.handle_streamable_http)

The MCP server in Streamable HTTP will be available under: http://127.0.0.1:8000/mcp/

You could for example use that to only expose the MCP server.

Code

You can find self contained examples code in my python-examples repo, under python_examples/ai/mcp/gradio.

Conclusion

I learned quite bit about the inside of Gradio putting this together. I hope you did too. Gradio is certainly a powerful tool, and it’s MCP support is only adding to it.

If you are interested in providing a Gradio app, maybe hosting it on Hugging Face Spaces, then using it for the MCP server as well might be the path with the least friction.

However, if your primarily focus is the MCP server itself, then personally I’d currently still lean more towards using FastMCP (via the official MCP Python SDK or FastMCP v2) - mainly because it seems easier to create a tools with the proper schema and documentation.

Subscribe