discourse/plugins/discourse-ai/spec/lib/mcp/tool_registry_spec.rb
Sam b2d73b346d
FEATURE: Add MCP server integration to AI agents (#38706)
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
2026-03-25 17:32:27 +11:00

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