MCP服务器示例

一个基于Rails 8的MCP服务器示例,展示如何通过OpenAI Apps SDK构建交互式UI组件,支持实时流传输和会话管理。

  • by jasich
  • Ruby
  • 本地部署
  • 1 星标
  • 更新于
在 GitHub 查看 jasich/football-mcp

使用说明

足球MCP服务器

一个Rails 8应用程序示例,演示如何构建 模型上下文协议 使用交互式UI组件的服务器 官方Ruby MCP SDK 以及OpenAI Apps SDK。

✨ OpenAI应用SDK示例 -此应用程序重点介绍了如何使用OpenAI Apps SDK为Rails MCP服务器构建UI组件,包括使用流式HTTP传输和服务器发送事件(SSE)在ChatGPT内呈现的React小部件。

特性

  • 可流式HTTP传输 -ChatGPT集成的推荐传输方式
  • 服务器发送事件(SSE) -工具响应的实时流式传输
  • 会话管理 -正确处理多个并发客户端
  • React 19小部件 -在ChatGPT内呈现的交互式UI
  • 组件注册表模式 -用于快速开发的可重用小部件架构
  • 即将到来的日程小部件 -基于OpenAI Apps SDK的交互式购票CTA
  • 小部件内跟进 -实时分数小部件可以触发后续提示(通过 sendFollowUpMessage)无需键入即可获取更深入的数据

⚠️ 生产警告

此应用程序尚未准备好生产。 它使用内存中的会话存储:

  • 不适用于多个服务器或进程
  • 重新启动时丢失所有会话
  • 仅适用于开发、演示和单服务器部署

对于生产使用,实现Redis支持的会话或使用无状态模式。

截图

实时分数小工具

Live Scores Widget在ChatGPT内呈现的交互式React组件中显示实时足球比赛分数。

团队信息资源

球队信息资源提供结构化的NFL球队数据,包括分区、颜色和体育场信息。

设置

bundle install
npm install
cp .env.example .env
# Edit .env and set BASE_URL to your tunnel URL for ChatGPT testing

启动服务器:

# Recommended: Start everything with one command (Rails + JS build + Cloudflare Tunnel)
# Note: Requires cloudflared to be installed and configured
bin/dev

# Alternative: For local testing only (no HTTPS tunnel, no auto-rebuild)
bin/rails server

手动构建React小部件:

# One-time build
npm run build

# Watch mode (rebuilds on changes) - not needed if using bin/dev
npm run watch

运行测试:

bin/rails test

运行门楣:

rubocop

ChatGPT/OpenAI应用SDK集成

HTTPS隧道开发

ChatGPT需要HTTPS端点才能连接到本地开发服务器。您有几个选择:

选项1:Cloudflare隧道(推荐)

  • 无浏览器警告或插播
  • 使用您自己的域名
  • 免费套餐可用
  • 安装 cloudflared 并在中进行配置 Procfile.dev:
    tunnel: cloudflared tunnel run your-tunnel-name
    
  • 长时间开发会话更稳定

选项2:ngrok(付费计划)

  • 免费版显示浏览器警告 中断ChatGPT集成
  • 付费计划(每月8美元以上)删除浏览器警告
  • 简单设置: ngrok http 3000
  • 适合使用付费帐户进行快速测试

选项3:其他隧道服务

  • localhost.run -基于SSH的隧道,无需安装
  • 尾鳞片漏斗 -如果您使用Tailscale进行网络连接
  • 钻孔 -基于Rust的开源替代方案
  • VS代码端口转发 -如果使用GitHub代码空间或VS代码远程

重要提示: 免费ngrok帐户显示一个间隙“访问网站”警告页面,阻止ChatGPT加载您的资源。您需要一个付费的ngrok帐户或另一个隧道解决方案。

设置ChatGPT

  1. 配置Cloudflare隧道 在 Procfile.dev:

    tunnel: cloudflared tunnel run your-tunnel-name
    
  2. 从开始 bin/dev -这将运行Rails、JS watch和你的隧道

  3. 查找您的隧道URL 从Cloudflare仪表板进行更新 .env:

    BASE_URL=https://your-tunnel-url.com
    
  4. 重启 bin/dev 使BASE_URL更改生效

添加工具

工具提供客户端可以调用的可调用函数。

步骤1:在中创建一个新的工具类 app/mcp_tools/

class MyTool < MCP::Tool
  title "My Tool"
  description "What this tool does"

  input_schema(
    type: "object",
    properties: { param: { type: "string" } },
    required: ["param"]
  )

  def self.call(param:, server_context: nil)
    MCP::Tool::Response.new([{ "type" => "text", "text" => "Result: #{param}" }])
  end
end

步骤2:在中注册工具 app/controllers/mcp_controller.rb

将其添加到 tools: 数组中 create_transport 方法:

tools: [GetLiveScoresTool, MyTool]

当前工具

  • GetLiveScoresTool (app/mcp_tools/get_live_scores_tool.rb):返回带有可选联赛过滤的模拟实况足球比分。演示响应格式和数据过滤模式。
  • GetTeamInfo工具 (app/mcp_tools/get_team_info_tool.rb):返回有关NFL球队的信息,包括分区、颜色和体育场详细信息。演示结构化数据响应。

添加资源

资源提供客户端可以访问的数据。它们可以是纯文本、JSON、HTML小部件或任何其他内容类型。

步骤1:在中创建新的资源类 app/mcp_resources/

class MyResource
  VERSION = "v1"  # Increment to v2, v3, etc. when content changes
  URI = "my-resource://data?#{VERSION}"

  class << self
    def to_resource
      MCP::Resource.new(
        uri: URI,
        name: "My Resource",
        description: "What this resource provides",
        mime_type: "text/plain"
      )
    end

    def read
      "Resource content here"
    end
  end
end

步骤2:在中注册资源 app/controllers/mcp_controller.rb

添加 MyResource.to_resource 到 resources: 数组in create_transport:

resources: [LiveScoresWidgetResource.to_resource, MyResource.to_resource]

添加一个 when MyResource::URI 案例中 resources_read_handler 块:

resources_read_handler: lambda { |uri, server_context|
  case uri
  when LiveScoresWidgetResource::URI
    LiveScoresWidgetResource.read
  when MyResource::URI
    MyResource.read
  else
    raise MCP::Error.new(
      code: MCP::JSONRPC::ErrorCodes::INVALID_PARAMS,
      message: "Unknown resource URI: #{uri}"
    )
  end
}

重要提示:资源版本控制

ChatGPT和其他MCP客户端会积极缓存资源。始终在资源URI中包含版本参数(例如。, ?v1, ?v2)并在每次修改资源内容时递增:

class MyResource
  VERSION = "v1"  # Increment to v2, v3, etc. when content changes
  URI = "my-resource://data?#{VERSION}"
end

如果没有版本控制,客户端将无限期地继续使用过时的缓存版本,即使在服务器重新启动后也是如此。

添加新的React小部件

添加新小部件只需要 6个步骤:

  1. 创建React组件
  2. 将其添加到组件注册表(中的一行 application.js)
  3. 创建一个使用共享模板的资源类
  4. 在MCP控制器中注册资源
  5. 重建资产
  6. 测试

无需单独的ERB模板! 所有小部件共享 app/views/mcp_widgets/widget.html.erb.

步骤1:创建一个新的React组件

在中创建组件文件 app/javascript/components/:

// app/javascript/components/MyWidget.jsx
import React from 'react';
import { useToolOutput } from '../utils/openai-hooks';

const MyWidget = () => {
  const toolOutput = useToolOutput();

  if (!toolOutput) {
    return <p>Waiting for data...</p>;
  }

  return (
    <div className="font-sans p-5 bg-white dark:bg-gray-950 text-gray-900 dark:text-gray-100 min-h-screen">
      <div className="max-w-2xl mx-auto">
        <h2 className="text-2xl font-bold mb-4">My Widget</h2>
        <pre className="bg-gray-100 dark:bg-gray-800 p-4 rounded-lg overflow-auto">
          {JSON.stringify(toolOutput, null, 2)}
        </pre>
      </div>
    </div>
  );
};

export default MyWidget;

步骤2:在中注册组件 app/javascript/application.js

只需将您的组件添加到 COMPONENT_REGISTRY:

import MyWidget from './components/MyWidget';

const COMPONENT_REGISTRY = {
  'LiveScoresWidget': LiveScoresWidget,
  'MyWidget': MyWidget,  // Add this line
};

步骤3:在中创建资源类 app/mcp_resources/

# app/mcp_resources/my_widget_resource.rb
class MyWidgetResource
  VERSION = "v1"
  URI = "ui://widget/my-widget.html?#{VERSION}"

  class << self
    def to_resource
      MCP::Resource.new(
        uri: URI,
        name: "My Widget",
        description: "Custom widget for displaying data",
        mime_type: "text/html+skybridge"
      )
    end

    def read
      ActionController::Base.render(
        template: "mcp_widgets/widget",  # Shared template
        layout: false,
        locals: {
          widget_title: "My Widget",
          component_name: "MyWidget"  # Must match COMPONENT_REGISTRY key
        }
      )
    end

    def meta
      base_url = ENV.fetch("BASE_URL", "http://localhost:3000")

      {
        "openai/widgetPrefersBorder" => true,
        "openai/widgetDomain" => "https://chatgpt.com",
        "openai/widgetCSP" => {
          "connect_domains" => [ "https://chatgpt.com", base_url ],
          "resource_domains" => [ base_url, "https://*.oaistatic.com" ]
        }
      }
    end
  end
end

步骤4:在中注册资源 app/controllers/mcp_controller.rb

# Add to the resources: array in create_transport
resources: [
  LiveScoresWidgetResource.to_resource,
  MyWidgetResource.to_resource
]

# Add to the resources_read_handler block
resources_read_handler: lambda { |uri, server_context|
  case uri
  when LiveScoresWidgetResource::URI
    LiveScoresWidgetResource.read
  when MyWidgetResource::URI
    MyWidgetResource.read
  else
    raise MCP::Error.new(
      code: MCP::JSONRPC::ErrorCodes::INVALID_PARAMS,
      message: "Unknown resource URI: #{uri}"
    )
  end
}

步骤5:重建资产并测试

# Rebuild JavaScript
npm run build

# Increment VERSION in MyWidgetResource to v2, v3, etc. when making changes
# This forces ChatGPT to fetch the updated widget

步骤6:在ChatGPT中测试

将您的MCP服务器添加到ChatGPT,然后在对话中引用您的新资源。ChatGPT将在iframe中加载您的小部件。

测试

# Run all tests
bin/rails test

# Run specific test file
bin/rails test test/controllers/mcp_controller_test.rb

# Run with verbose output
bin/rails test -v

项目结构

app/
├── controllers/
│   └── mcp_controller.rb          # Main MCP endpoint handler
├── mcp_tools/                      # MCP tool implementations
│   ├── get_live_scores_tool.rb
│   └── get_team_info_tool.rb
├── mcp_resources/                  # MCP resource implementations
│   ├── live_scores_widget_resource.rb
│   └── team_info_resource.rb
├── javascript/
│   ├── application.js              # Component registry & mounting
│   └── components/                 # React components
│       └── LiveScoresWidget.jsx
└── views/
    └── mcp_widgets/
        └── widget.html.erb         # Shared widget template

config/
├── routes.rb                       # MCP endpoint routes
└── initializers/
    └── cors.rb                     # CORS configuration

test/
└── controllers/
    └── mcp_controller_test.rb      # MCP endpoint tests

许可证

麻省理工学院