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:
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 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:
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
.
Dropdown
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:
Both tools are listed as MCP 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.