mirror of
https://github.com/opf/openproject.git
synced 2026-06-14 03:30:14 +00:00
Add tools that return the content of certain resources
They are useful for clients that don't support resources, or ignore them, even if they could support them.
This commit is contained in:
@@ -60,17 +60,21 @@ module McpResources
|
||||
end
|
||||
|
||||
def read_resource(uri)
|
||||
resource_class = enabled.find { |r| r.uri == uri || r.uri_template&.match?(uri) }
|
||||
content = resource_class&.read(uri)
|
||||
content = read_resource_content(uri)
|
||||
return [] if content.nil?
|
||||
|
||||
[
|
||||
{
|
||||
uri: uri,
|
||||
mimeType: "application/json",
|
||||
text: content.to_json
|
||||
}
|
||||
format_json_resource(uri, content)
|
||||
]
|
||||
end
|
||||
|
||||
def read_resource_content(uri, resources_considered: enabled)
|
||||
resource_class = resources_considered.find { |r| r.uri == uri || r.uri_template&.match?(uri) }
|
||||
resource_class&.read(uri)
|
||||
end
|
||||
|
||||
def format_json_resource(uri, content)
|
||||
{ uri:, mimeType: "application/json", text: content.to_json }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -32,6 +32,8 @@ module McpTools
|
||||
class << self
|
||||
def all
|
||||
[
|
||||
McpTools::ListStatuses,
|
||||
McpTools::ListTypes,
|
||||
McpTools::SearchProjects,
|
||||
McpTools::SearchWorkPackages
|
||||
]
|
||||
|
||||
@@ -113,7 +113,7 @@ module McpTools
|
||||
|
||||
def read_annotations
|
||||
# Initialize default annotations, if none are present
|
||||
annotations(read_only: false) if @annotations.nil?
|
||||
annotations(read_only: false, destructive: true, idempotent: false) if @annotations.nil?
|
||||
|
||||
@annotations
|
||||
end
|
||||
@@ -151,7 +151,7 @@ module McpTools
|
||||
validate_root_output_schema!(@tool_context.output_schema)
|
||||
end
|
||||
|
||||
MCP::Tool::Response.new([{ type: "text", text: result.to_json }], structured_content: result)
|
||||
format_result(result)
|
||||
end
|
||||
|
||||
private
|
||||
@@ -161,6 +161,10 @@ module McpTools
|
||||
raise NotImplemented, "#{self.class} needs to implement #call method"
|
||||
end
|
||||
|
||||
def format_result(result)
|
||||
MCP::Tool::Response.new([{ type: "text", text: result.to_json }], structured_content: result)
|
||||
end
|
||||
|
||||
def current_user
|
||||
@server_context[:current_user]
|
||||
end
|
||||
|
||||
@@ -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 McpTools
|
||||
class ListStatuses < ResourceProxyTool
|
||||
default_title "List statuses"
|
||||
default_description "Lists all work package statuses available on this OpenProject instance. " \
|
||||
"Also available as an MCP resource."
|
||||
|
||||
name "list_statuses"
|
||||
|
||||
resource McpResources::StatusList
|
||||
resource_schema "status_collection_model"
|
||||
resource_annotations
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,42 @@
|
||||
# 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 McpTools
|
||||
class ListTypes < ResourceProxyTool
|
||||
default_title "List types"
|
||||
default_description "Lists all work package types available on this OpenProject instance. Also available as an MCP resource."
|
||||
|
||||
name "list_types"
|
||||
|
||||
resource McpResources::TypeList
|
||||
resource_schema "types_model"
|
||||
resource_annotations
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,64 @@
|
||||
# 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 McpTools
|
||||
# A tool that effectively only serves a result that could've been obtained by fetching a resource
|
||||
# Useful for clients that don't support resources, or ignore them, even if they could support them.
|
||||
class ResourceProxyTool < Base
|
||||
class << self
|
||||
def resource(resource = nil)
|
||||
@resource = resource if resource.present?
|
||||
|
||||
@resource
|
||||
end
|
||||
|
||||
def resource_schema(schema_definition)
|
||||
output_schema(JsonSchemaLoader.new.load(schema_definition))
|
||||
end
|
||||
|
||||
def resource_annotations
|
||||
annotations read_only: true, idempotent: true, destructive: false
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def call
|
||||
McpResources.read_resource_content(self.class.resource.uri, resources_considered: McpResources.all)
|
||||
end
|
||||
|
||||
def format_result(content)
|
||||
MCP::Tool::Response.new(
|
||||
[{ type: "resource", resource: McpResources.format_json_resource(self.class.resource.uri, content) }],
|
||||
structured_content: content
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,94 @@
|
||||
# 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.
|
||||
#++
|
||||
|
||||
require "spec_helper"
|
||||
|
||||
RSpec.describe McpTools::ListStatuses, with_flag: { mcp_server: true } do
|
||||
subject do
|
||||
header "Authorization", "Bearer #{access_token.plaintext_token}"
|
||||
header "X-Authentication-Scheme", "Bearer"
|
||||
header "Content-Type", "application/json"
|
||||
post "/mcp", request_body.to_json
|
||||
end
|
||||
|
||||
let(:access_token) { create(:oauth_access_token, scopes: "mcp", resource_owner: user) }
|
||||
let(:user) { create(:user) }
|
||||
let(:request_body) do
|
||||
{
|
||||
jsonrpc: "2.0",
|
||||
id: "Test-Request",
|
||||
method: "tools/call",
|
||||
params: {
|
||||
name: "list_statuses",
|
||||
arguments: call_args
|
||||
}
|
||||
}
|
||||
end
|
||||
let(:call_args) { {} }
|
||||
let(:parsed_results) { JSON.parse(last_response.body).fetch("result") }
|
||||
|
||||
let!(:status_a) { create(:status) }
|
||||
let!(:status_b) { create(:status) }
|
||||
|
||||
let(:server_config) { create(:mcp_configuration, identifier: "mcp_server") }
|
||||
let(:tool_config) { create(:mcp_configuration, identifier: described_class.qualified_name) }
|
||||
|
||||
before do
|
||||
server_config.save!
|
||||
tool_config.save!
|
||||
end
|
||||
|
||||
context "when the mcp_server enterprise feature is enabled", with_ee: %i[mcp_server] do
|
||||
it_behaves_like "MCP response with structured content"
|
||||
|
||||
it "finds all statuses" do
|
||||
subject
|
||||
expect(parsed_results.dig("structuredContent", "count")).to eq(2)
|
||||
end
|
||||
|
||||
it "responds with properly formatted statuses" do
|
||||
subject
|
||||
expect(parsed_results.fetch("structuredContent").to_json).to match_json_schema.from_docs("status_collection_model")
|
||||
end
|
||||
|
||||
context "when the tool is disabled via configuration" do
|
||||
let(:tool_config) { create(:mcp_configuration, identifier: described_class.qualified_name, enabled: false) }
|
||||
|
||||
it_behaves_like "MCP error response"
|
||||
end
|
||||
end
|
||||
|
||||
context "when the mcp_server enterprise feature is disabled" do
|
||||
it "responds in a 404" do
|
||||
subject
|
||||
expect(last_response).to have_http_status(404)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,94 @@
|
||||
# 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.
|
||||
#++
|
||||
|
||||
require "spec_helper"
|
||||
|
||||
RSpec.describe McpTools::ListTypes, with_flag: { mcp_server: true } do
|
||||
subject do
|
||||
header "Authorization", "Bearer #{access_token.plaintext_token}"
|
||||
header "X-Authentication-Scheme", "Bearer"
|
||||
header "Content-Type", "application/json"
|
||||
post "/mcp", request_body.to_json
|
||||
end
|
||||
|
||||
let(:access_token) { create(:oauth_access_token, scopes: "mcp", resource_owner: user) }
|
||||
let(:user) { create(:user) }
|
||||
let(:request_body) do
|
||||
{
|
||||
jsonrpc: "2.0",
|
||||
id: "Test-Request",
|
||||
method: "tools/call",
|
||||
params: {
|
||||
name: "list_types",
|
||||
arguments: call_args
|
||||
}
|
||||
}
|
||||
end
|
||||
let(:call_args) { {} }
|
||||
let(:parsed_results) { JSON.parse(last_response.body).fetch("result") }
|
||||
|
||||
let!(:type_a) { create(:type) }
|
||||
let!(:type_b) { create(:type) }
|
||||
|
||||
let(:server_config) { create(:mcp_configuration, identifier: "mcp_server") }
|
||||
let(:tool_config) { create(:mcp_configuration, identifier: described_class.qualified_name) }
|
||||
|
||||
before do
|
||||
server_config.save!
|
||||
tool_config.save!
|
||||
end
|
||||
|
||||
context "when the mcp_server enterprise feature is enabled", with_ee: %i[mcp_server] do
|
||||
it_behaves_like "MCP response with structured content"
|
||||
|
||||
it "finds all types" do
|
||||
subject
|
||||
expect(parsed_results.dig("structuredContent", "count")).to eq(2)
|
||||
end
|
||||
|
||||
it "responds with properly formatted types" do
|
||||
subject
|
||||
expect(parsed_results.fetch("structuredContent").to_json).to match_json_schema.from_docs("types_model")
|
||||
end
|
||||
|
||||
context "when the tool is disabled via configuration" do
|
||||
let(:tool_config) { create(:mcp_configuration, identifier: described_class.qualified_name, enabled: false) }
|
||||
|
||||
it_behaves_like "MCP error response"
|
||||
end
|
||||
end
|
||||
|
||||
context "when the mcp_server enterprise feature is disabled" do
|
||||
it "responds in a 404" do
|
||||
subject
|
||||
expect(last_response).to have_http_status(404)
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user