mirror of
https://gh.wpcy.net/https://github.com/discourse/discourse.git
synced 2026-06-19 07:43:46 +08:00
Introduce support for Model Context Protocol (MCP) servers in the discourse-ai plugin, allowing AI agents to connect to external tool servers via the MCP standard. Key additions: - AiMcpServer model with CRUD admin UI, health tracking, and tool caching (hourly refresh via scheduled job) - MCP client (Streamable HTTP transport) with session management and tool invocation - Full OAuth 2.1 flow support (discovery, dynamic registration, authorization code grant, token refresh, and disconnect) - MCP tool type for AI agents that proxies tool calls to remote MCP servers at runtime - Agent editor updated to show combined tool/token counts from both local tools and MCP servers - Agent import/export includes MCP server associations - Admin secrets UI updated to surface MCP server usage - Comprehensive specs for models, controllers, client, tool registry, and OAuth flow
81 lines
2.5 KiB
Ruby
Vendored
81 lines
2.5 KiB
Ruby
Vendored
# frozen_string_literal: true
|
|
|
|
RSpec.describe DiscourseAi::Mcp::ToolRegistry do
|
|
before { enable_current_plugin }
|
|
|
|
describe ".cache_key" do
|
|
it "namespaces the cache by current multisite database" do
|
|
RailsMultisite::ConnectionManagement.stubs(:current_db).returns("second")
|
|
|
|
expect(described_class.cache_key(42)).to eq("discourse-ai:mcp-tools:v1:second:42")
|
|
end
|
|
end
|
|
|
|
describe ".tool_classes_for_servers" do
|
|
fab!(:first_server) { Fabricate(:ai_mcp_server, name: "Jira") }
|
|
fab!(:second_server) { Fabricate(:ai_mcp_server, name: "GitHub") }
|
|
|
|
it "namespaces colliding tool names" do
|
|
described_class
|
|
.stubs(:tool_definitions_for)
|
|
.with(first_server)
|
|
.returns([{ "name" => "search", "description" => "Search Jira", "inputSchema" => {} }])
|
|
described_class
|
|
.stubs(:tool_definitions_for)
|
|
.with(second_server)
|
|
.returns([{ "name" => "search", "description" => "Search GitHub", "inputSchema" => {} }])
|
|
|
|
classes =
|
|
described_class.tool_classes_for_servers(
|
|
[first_server, second_server],
|
|
reserved_names: ["search"],
|
|
)
|
|
|
|
expect(classes.map { |klass| klass.signature[:name] }).to contain_exactly(
|
|
"jira__search",
|
|
"github__search",
|
|
)
|
|
end
|
|
|
|
it "ignores disconnected oauth servers" do
|
|
disconnected_oauth_server =
|
|
Fabricate(
|
|
:ai_mcp_server,
|
|
name: "OAuth Docs",
|
|
auth_type: "oauth",
|
|
oauth_status: "disconnected",
|
|
)
|
|
|
|
described_class
|
|
.stubs(:tool_definitions_for)
|
|
.with(disconnected_oauth_server)
|
|
.returns([{ "name" => "search", "description" => "Search docs", "inputSchema" => {} }])
|
|
|
|
classes = described_class.tool_classes_for_servers([disconnected_oauth_server])
|
|
|
|
expect(classes).to eq([])
|
|
end
|
|
|
|
it "filters tool classes by selected tool names" do
|
|
described_class
|
|
.stubs(:tool_definitions_for)
|
|
.with(first_server)
|
|
.returns(
|
|
[
|
|
{ "name" => "search", "description" => "Search Jira", "inputSchema" => {} },
|
|
{ "name" => "create", "description" => "Create Jira", "inputSchema" => {} },
|
|
],
|
|
)
|
|
|
|
classes =
|
|
described_class.tool_classes_for_servers(
|
|
[first_server],
|
|
selected_tool_names_by_server: {
|
|
first_server.id => ["create"],
|
|
},
|
|
)
|
|
|
|
expect(classes.map { |klass| klass.signature[:name] }).to eq(["create"])
|
|
end
|
|
end
|
|
end
|