ChatGPT 中的应用是一种很好的方式,让用户可以通过在熟悉的聊天应用中聊天来试用您的机器学习模型或其他类型的应用。OpenAI 已经发布了 Apps SDK 供开发者构建完整的应用,但您可以利用 Gradio MCP 服务器,非常快速地使用 Gradio 来构建 ChatGPT 应用。我们还将看到 Gradio 内置的分享链接如何使您的 ChatGPT 应用的迭代变得格外容易!
构建 ChatGPT 应用需要完成两件事情:
构建一个 Gradio MCP 服务器,并暴露至少一个工具。如果您还不熟悉如何构建 Gradio MCP 服务器,我们建议您先阅读本指南。
使用 HTML、JavaScript 和 CSS 构建一个自定义 UI,当您的工具被调用时将显示该 UI,并将其作为 MCP 资源暴露。
我们将在下面详细介绍这些步骤。
mcp 附加组件的 gradio>=6.0pip install --upgrade gradio[mcp]现在,让我们通过两个例子来了解如何使用 Gradio 构建 ChatGPT 应用。
第一个示例是一个 ChatGPT 应用,用于计算单词中字母的出现次数,并显示一张卡片,其中包含单词和高亮显示的指定字母,如下所示:
那么我们如何构建它呢?您可以在这里找到字母计数器应用的完整代码,也可以按照以下步骤操作:
def letter_counter(word: str, letter: str) -> int:
"""
Count the number of letters in a word or phrase.
Parameters:
word (str): The word or phrase to count the letters of.
letter (str): The letter to count the occurrences of.
"""
return word.count(letter)with gr.Blocks() as demo:
with gr.Row():
with gr.Column():
word = gr.Textbox(label="Word")
letter = gr.Textbox(label="Letter")
btn = gr.Button("Count Letters")
with gr.Column():
count = gr.Number(label="Count")
btn.click(letter_counter, inputs=[word, letter], outputs=count)mcp_server=True: demo.launch(mcp_server=True)如早期指南所述,您现在可以使用任何 MCP 客户端(例如 MCP Inspector 工具)来测试该工具。进行测试并确认其行为符合您的预期。
window.openai?.toolInput?.word 和 window.openai?.toolInput?.letter 来访问用户的工具输入。window.openai 对象由 ChatGPT 自动插入,其中包含来自用户工具调用的数据。这是完整的函数代码:@gr.mcp.resource("ui://widget/app.html", mime_type="text/html+skybridge")
def app_html():
visual = """
<div id="letter-card-container"></div>
<script>
const container = document.getElementById('letter-card-container');
function render() {
const word = window.openai?.toolInput?.word || "strawberry";
const letter = window.openai?.toolInput?.letter || "r";
let letterHTML = '';
for (let i = 0; i < word.length; i++) {
const char = word[i];
const color = char.toLowerCase() === letter.toLowerCase() ? '#b8860b' : '#000000';
letterHTML += `<span style="color: ${color};">${char}</span>`;
}
container.innerHTML = `
<div style="
background: linear-gradient(135deg, #f5f5dc 0%, #e8e4d0 100%);
background-image:
repeating-linear-gradient(45deg, transparent, transparent 2px, rgba(139, 121, 94, 0.03) 2px, rgba(139, 121, 94, 0.03) 4px),
linear-gradient(135deg, #f5f5dc 0%, #e8e4d0 100%);
border-radius: 16px;
padding: 40px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1), 0 1px 3px rgba(0, 0, 0, 0.08);
max-width: 600px;
margin: 20px auto;
font-family: 'Georgia', serif;
text-align: center;
">
<div style="
font-size: 48px;
font-weight: bold;
letter-spacing: 8px;
line-height: 1.5;
">
${letterHTML}
</div>
</div>
`;
}
render();
window.addEventListener("openai:set_globals", (event) => {
if (event.detail?.globals?.toolInput) {
render();
}
}, { passive: true });
</script>
"""
return visual请注意,我们为 gr.mcp.resource 提供了 URI ui://widget/app.html。这是任意的,但我们稍后需要使用相同的 URI。我们还需要将资源的 mimetype 指定为 mime_type="text/html+skybridge"。最后,请注意,我们在 JavaScript 中为 "openai:set_globals" 添加了一个事件监听器,这通常是一个好习惯,因为它允许小部件在触发新的工具调用时更新。
gr.Code 组件中显示 MCP 资源的代码,例如这样: html = gr.Code(language="html", max_lines=20)
# ... the rest of your Gradio app
btn.click(app_html, outputs=html)_meta 属性。我们需要将我们创建的 MCP 工具与我们为应用创建的 UI 连接起来。我们可以通过将此装饰器添加到我们的 MCP 工具函数中来实现:@gr.mcp.tool(
_meta={
"openai/outputTemplate": "ui://widget/app.html",
"openai/resultCanProduceWidget": True,
"openai/widgetAccessible": True,
}
)需要注意的关键是 "openai/outputTemplate" 必须与我们之前创建的 MCP 资源的 URI 匹配。
share=True 重新启动您的 Gradio 应用。这将使在 ChatGPT 中进行测试变得非常容易。请注意打印到终端的 MCP 服务器 URL,例如 https://2e879c6066d729b11b.gradio.live/gradio_api/mcp/。 demo.launch(share=True, mcp_server=True)这将打印一个公共 URL,您的 Gradio 应用将在此 URL 上运行。

就是这样!连接器创建完成后,您可以通过说“使用 @letter-counter 来计算 Gradio 中 r 的数量”之类的话来开始提示它。
接下来,让我们看一个更复杂的 ChatGPT 应用,用于图像增强。这个 ChatGPT 应用包含一个“增亮”按钮,允许用户直接从应用 UI 调用工具。
这是此应用的完整代码:
import gradio as gr
import tempfile
from PIL import Image
import numpy as np
@gr.mcp.tool(
_meta={
"openai/outputTemplate": "ui://widget/app.html",
"openai/resultCanProduceWidget": True,
"openai/widgetAccessible": True,
}
)
def power_law_image(input_path: str, gamma: float = 0.5) -> str:
"""
Applies a power-law (gamma) transformation to an image file and saves
the result to a temporary file.
Args:
input_path (str): Path to the input image.
gamma (float): Power-law exponent. <1 brightens, >1 darkens.
Returns:
str: Path to the saved temporary output image.
"""
img = Image.open(input_path).convert("RGB")
arr = np.array(img, dtype=np.float32) / 255.0
arr = np.power(arr, gamma)
arr = np.clip(arr * 255, 0, 255).astype(np.uint8)
out_img = Image.fromarray(arr)
tmp_file = tempfile.NamedTemporaryFile(delete=False, suffix=".png")
out_img.save(tmp_file.name)
tmp_file.close()
return tmp_file.name
@gr.mcp.resource("ui://widget/app.html", mime_type="text/html+skybridge")
def app_html():
visual = """
<style>
#image-container {
position: relative;
display: inline-block;
max-width: 100%;
}
#image-display {
max-width: 100%;
height: auto;
display: block;
border-radius: 8px;
}
#brighten-btn {
position: absolute;
bottom: 16px;
right: 26px;
padding: 12px 24px;
background: #1a1a1a;
color: white;
border: none;
border-radius: 8px;
font-weight: 600;
cursor: pointer;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
}
#brighten-btn:hover {
background: #000000;
}
</style>
<div id="image-container">
<img id="image-display" alt="Processed image" />
<button id="brighten-btn">Brighten</button>
</div>
<script>
const imageEl = document.getElementById('image-display');
const btnEl = document.getElementById('brighten-btn');
function extractImageUrl(data) {
if (data?.text?.startsWith('Image URL: ')) {
return data.text.substring('Image URL: '.length).trim();
}
if (data?.content) {
for (const item of data.content) {
if (item.type === 'text' && item.text?.startsWith('Image URL: ')) {
return item.text.substring('Image URL: '.length).trim();
}
}
}
}
function render() {
const url = extractImageUrl(window.openai?.toolOutput);
if (url) imageEl.src = url;
}
async function brightenImage() {
btnEl.disabled = true;
btnEl.textContent = 'Brightening...';
const result = await window.openai.callTool('power_law_image', {
input_path: imageEl.src
});
const newUrl = extractImageUrl(result);
if (newUrl) imageEl.src = newUrl;
btnEl.disabled = false;
btnEl.textContent = 'Brighten';
}
btnEl.addEventListener('click', brightenImage);
window.addEventListener("openai:set_globals", (event) => {
if (event.detail?.globals?.toolOutput) render();
}, { passive: true });
render();
</script>
"""
return visual
with gr.Blocks() as demo:
with gr.Row():
with gr.Column():
original_image = gr.Image(label="Original Image", type="filepath")
btn = gr.Button("Brighten Image")
with gr.Column():
output_image = gr.Image(label="Output Image", type="filepath")
html = gr.Code(language="html", max_lines=20)
btn.click(power_law_image, inputs=original_image, outputs=original_image)
btn.click(app_html, outputs=html)
if __name__ == "__main__":
demo.launch(mcp_server=True, share=True)
我们不会像之前那样详细分解代码,因为许多部分是相同的。但请注意以下与上一个示例的不同之处:
window.openai.callTool() 直接从按钮点击调用 MCP 工具,而无需 ChatGPT 调用它。const result = await window.openai.callTool('power_law_image', {
input_path: imageEl.src
});callTool() 的结果包含一个 content 数组,需要解析以提取数据。function extractImageUrl(data) {
if (data?.content) {
for (const item of data.content) {
if (item.type === 'text' && item.text?.startsWith('Image URL: ')) {
return item.text.substring('Image URL: '.length).trim();
}
}
}
}const newUrl = extractImageUrl(result);
if (newUrl) imageEl.src = newUrl;通过这些示例,您已经了解了如何构建简单的反应式小部件和更高级的交互式应用,这些应用可以直接从 UI 调用工具。通过将 Gradio 的 MCP 服务器功能与 OpenAI Apps SDK 相结合,是时候开始创建更丰富的 ChatGPT 集成,通过自定义可视化和用户交互来增强对话体验了!