mirror of
https://gh.wpcy.net/https://github.com/discourse/discourse.git
synced 2026-06-19 02:05:37 +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
93 lines
2.8 KiB
Ruby
Vendored
93 lines
2.8 KiB
Ruby
Vendored
# frozen_string_literal: true
|
|
|
|
class AiSecret < ActiveRecord::Base
|
|
belongs_to :created_by, class_name: "User", optional: true
|
|
|
|
validates :name, presence: true, uniqueness: true, length: { maximum: 100 }
|
|
validates :secret, presence: true, length: { maximum: 10_000 }
|
|
|
|
has_many :llm_models, dependent: :nullify
|
|
has_many :embedding_definitions, dependent: :nullify
|
|
has_many :ai_tool_secret_bindings, dependent: :destroy
|
|
has_many :ai_mcp_servers, dependent: :nullify
|
|
has_many :oauth_client_secret_mcp_servers,
|
|
class_name: "AiMcpServer",
|
|
foreign_key: :oauth_client_secret_ai_secret_id,
|
|
dependent: :nullify
|
|
|
|
def in_use?
|
|
llm_models.exists? || embedding_definitions.exists? || used_by_provider_params? ||
|
|
ai_tool_secret_bindings.exists? || ai_mcp_servers.exists? ||
|
|
oauth_client_secret_mcp_servers.exists?
|
|
end
|
|
|
|
def used_by
|
|
usage = []
|
|
llm_models.each { |llm| usage << { type: "llm", name: llm.display_name, id: llm.id } }
|
|
embedding_definitions.each do |ed|
|
|
usage << { type: "embedding", name: ed.display_name, id: ed.id }
|
|
end
|
|
provider_param_llms.each do |llm|
|
|
usage << { type: "llm_provider_param", name: llm.display_name, id: llm.id }
|
|
end
|
|
ai_tool_secret_bindings
|
|
.includes(:ai_tool)
|
|
.each do |binding|
|
|
next if binding.ai_tool.blank?
|
|
|
|
usage << {
|
|
type: "tool",
|
|
name: "#{binding.ai_tool.name} (#{binding[:alias]})",
|
|
id: binding.ai_tool.id,
|
|
}
|
|
end
|
|
ai_mcp_servers.each do |server|
|
|
usage << { type: "mcp_server", name: server.name, id: server.id }
|
|
end
|
|
oauth_client_secret_mcp_servers.each do |server|
|
|
usage << { type: "mcp_server_oauth_client_secret", name: server.name, id: server.id }
|
|
end
|
|
usage
|
|
end
|
|
|
|
private
|
|
|
|
def used_by_provider_params?
|
|
provider_param_llms.exists?
|
|
end
|
|
|
|
def provider_param_llms
|
|
secret_keys =
|
|
LlmModel.provider_params.flat_map do |provider, params|
|
|
params.filter_map { |key, type| [provider, key] if type == :secret }
|
|
end
|
|
|
|
return LlmModel.none if secret_keys.empty?
|
|
|
|
sql_conditions = []
|
|
bind_values = []
|
|
|
|
secret_keys.each do |provider, key|
|
|
sql_conditions << "(provider = ? AND provider_params ->> ? = ?)"
|
|
bind_values << provider.to_s << key.to_s << id.to_s
|
|
end
|
|
|
|
LlmModel.where(sql_conditions.join(" OR "), *bind_values).where("llm_models.id > 0")
|
|
end
|
|
end
|
|
|
|
# == Schema Information
|
|
#
|
|
# Table name: ai_secrets
|
|
#
|
|
# id :bigint not null, primary key
|
|
# name :string(100) not null
|
|
# secret :string(10000) not null
|
|
# created_at :datetime not null
|
|
# updated_at :datetime not null
|
|
# created_by_id :integer
|
|
#
|
|
# Indexes
|
|
#
|
|
# index_ai_secrets_on_name (name) UNIQUE
|
|
#
|