Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions app/seeders/mcp_configuration_seeder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,11 @@ class McpConfigurationSeeder < Seeder
def seed_data!
seed_server_config if server_missing?

seed_tool_configs
seed_resource_and_tool_configs
end

def applicable?
server_missing? || tools_missing?
server_missing? || tools_missing? || resources_missing?
end

def not_applicable_message
Expand All @@ -53,8 +53,8 @@ def seed_server_config
)
end

def seed_tool_configs
McpTools.all.each do |thing| # rubocop:disable Rails/FindEach
def seed_resource_and_tool_configs
(McpTools.all + McpResources.all).each do |thing|
next if McpConfiguration.find_by(identifier: thing.qualified_name)

McpConfiguration.create!(
Expand All @@ -73,4 +73,8 @@ def server_missing?
def tools_missing?
(McpTools.all.map(&:qualified_name) - McpConfiguration.pluck(:identifier)).any?
end

def resources_missing?
(McpResources.all.map(&:qualified_name) - McpConfiguration.pluck(:identifier)).any?
end
end
76 changes: 76 additions & 0 deletions app/services/mcp_resources.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# frozen_string_literal: true

#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++

module McpResources
class << self
def all
[
Project,
Status,
StatusList,
Type,
TypeList,
User,
Version,
WorkPackage
]
end

def enabled
McpConfiguration.where(enabled: true).pluck(:identifier).filter_map { |name| resources_by_name[name] }
end

def resources_by_name
@resources_by_name ||= all.index_by(&:qualified_name)
end

def enabled_resources
enabled.select(&:uri)
end

def enabled_resource_templates
enabled.select(&:uri_template)
end

def read_resource(uri)
resource = enabled.find { |r| r.uri == uri || r.uri_template&.match?(uri) }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 use somehow a variable name, that indicates, that this is actually not a resource, but a resource class.

content = resource&.read(uri)
return [] if content.nil?

[
{
uri: uri,
mimeType: "application/json",
text: content.to_json
}
]
end
end
end
114 changes: 114 additions & 0 deletions app/services/mcp_resources/base.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
# frozen_string_literal: true

#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++

module McpResources
class Base
include APIV3Helper

class << self
def qualified_name
"resources/#{name}"
end

def default_title(title = nil)
@default_title = title if title.present?

@default_title
end

def default_description(description = nil)
@default_description = description if description.present?

@default_description
end

def name(name = nil)
@name = name if name.present?

@name
end

def uri(suffix = nil)
@uri_suffix = suffix if suffix.present?
return nil if @uri_suffix.nil?

"#{Setting.protocol}://#{Setting.host_name}#{@uri_suffix}"
end

def uri_template(suffix = nil)
@template_suffix = suffix if suffix.present?
return nil if @template_suffix.nil?

UriTemplate.new("#{Setting.protocol}://#{Setting.host_name}#{@template_suffix}")
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am open for ideas to improve efficiency here. I find it slightly sad that every call to uri_template creates a new UriTemplate instance (which involves compiling a regex).

On the other hand, we can't properly cache this in a simple instance variable: The setter version of this method is only called once, when the class gets first defined. However, the actual template should change immediately if the Setting.host_name changes (though it doesn't do that very often 🤷).

For now I am happy to live with the fact that this code is not 100% efficient, but it still makes me a tad sad.

end

def resource
raise ArgumentError, "#{self.class.name} can't be used as resource, uri is blank" if uri.blank?

config = McpConfiguration.find_by(identifier: qualified_name)
return nil if config.nil?

MCP::Resource.new(
uri:,
name:,
title: config.title,
description: config.description,
mime_type: "application/json"
)
end

def resource_template
raise ArgumentError, "#{self.class.name} can't be used as resource_template, uri_template is blank" if uri_template.blank?

config = McpConfiguration.find_by(identifier: qualified_name)
return nil if config.nil?

MCP::ResourceTemplate.new(
uri_template:,
name:,
title: config.title,
description: config.description,
mime_type: "application/json"
)
end

def read(uri)
params = uri_template&.parse(uri) || {}
new.read(**params)
end
end

def current_user = ::User.current

def read(**)
raise NotImplemented, "#{self.class} needs to implement #read method"
end
end
end
46 changes: 46 additions & 0 deletions app/services/mcp_resources/project.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# frozen_string_literal: true

#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++

module McpResources
class Project < Base
name "project"
uri_template "/api/v3/projects/{id}"

default_title "Project"
default_description "Access projects of this OpenProject instance."

def read(id:)
project = ::Project.visible(current_user).find_by(id:)
return nil if project.nil?

API::V3::Projects::ProjectRepresenter.create(project, current_user:)
end
end
end
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 I like how the specific resource in the end looks like. very small, easy to read.

46 changes: 46 additions & 0 deletions app/services/mcp_resources/status.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# frozen_string_literal: true

#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++

module McpResources
class Status < Base
name "status"
uri_template "/api/v3/statuses/{id}"

default_title "Work Package Status"
default_description "Access work package statuses of this OpenProject instance."

def read(id:)
status = ::Status.find_by(id:)
return nil if status.nil?

API::V3::Statuses::StatusRepresenter.new(status, current_user:)
end
end
end
43 changes: 43 additions & 0 deletions app/services/mcp_resources/status_list.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# frozen_string_literal: true

#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++

module McpResources
class StatusList < Base
name "status_list"
uri "/api/v3/statuses"

default_title "Work Package Statuses List"
default_description "A list of all work package statuses configured in this OpenProject instance."

def read
API::V3::Statuses::StatusCollectionRepresenter.new(::Status.all, self_link: api_v3_paths.statuses, current_user:)
end
end
end
Loading
Loading