mirror of
https://github.com/opf/openproject.git
synced 2026-06-13 19:20:00 +00:00
[#71358] add search version tool
- https://community.openproject.org/work_packages/71358 - add tool - add specs for the tool - slightly improve version model for the api
This commit is contained in:
@@ -39,6 +39,7 @@ module McpTools
|
||||
McpTools::SearchPrograms,
|
||||
McpTools::SearchProjects,
|
||||
McpTools::SearchUsers,
|
||||
McpTools::SearchVersions,
|
||||
McpTools::SearchWorkPackages
|
||||
]
|
||||
end
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
# 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 SearchVersions < Base
|
||||
default_title "Search versions"
|
||||
default_description "Search versions matching all of the passed input parameters. " \
|
||||
"Parameters not passed are ignored. Results are limited to a maximum " \
|
||||
"of #{page_size} versions. To get the rest of the results, call the tool again with a" \
|
||||
"page number of 2 or higher."
|
||||
|
||||
name "search_versions"
|
||||
annotations read_only: true, idempotent: true, destructive: false
|
||||
enable_pagination
|
||||
|
||||
filter :name, filter_class: Queries::Versions::Filters::NameFilter, operator: "~"
|
||||
filter :sharing
|
||||
|
||||
input_schema(
|
||||
type: :object,
|
||||
properties: {
|
||||
name: { type: "string", description: "Name of the version. Accepts partial version names, not case-sensitive." },
|
||||
sharing: {
|
||||
type: "string",
|
||||
enum: Version::VERSION_SHARINGS,
|
||||
description: "The indicator of how the version is shared between projects."
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
output_schema(
|
||||
type: :object,
|
||||
required: ["items"],
|
||||
properties: {
|
||||
items: {
|
||||
type: :array,
|
||||
items: JsonSchemaLoader.new.load("version_model")
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
def call(page: nil, **filters)
|
||||
filtered = apply_filters(Version.visible, filters)
|
||||
versions = apply_pagination(filtered, page)
|
||||
|
||||
{
|
||||
items: versions.map { |v| API::V3::Versions::VersionRepresenter.create(v, current_user:) }
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,25 +1,16 @@
|
||||
# Schema: VersionModel
|
||||
---
|
||||
type: object
|
||||
required:
|
||||
- name
|
||||
- status
|
||||
- sharing
|
||||
- createdAt
|
||||
- updatedAt
|
||||
properties:
|
||||
id:
|
||||
type: integer
|
||||
description: Version id
|
||||
readOnly: true
|
||||
exclusiveMinimum: 0
|
||||
minimum: 1
|
||||
name:
|
||||
type: string
|
||||
description: Version name
|
||||
description:
|
||||
allOf:
|
||||
- "$ref": "./formattable.yml"
|
||||
- {}
|
||||
$ref: "./formattable.yml"
|
||||
startDate:
|
||||
type:
|
||||
- "string"
|
||||
@@ -40,62 +31,55 @@ properties:
|
||||
type: string
|
||||
format: date-time
|
||||
description: Time of creation
|
||||
readOnly: true
|
||||
updatedAt:
|
||||
type: string
|
||||
format: date-time
|
||||
description: Time of the most recent change to the version
|
||||
readOnly: true
|
||||
_links:
|
||||
type: object
|
||||
required:
|
||||
- self
|
||||
- availableInProjects
|
||||
- self
|
||||
- availableInProjects
|
||||
properties:
|
||||
update:
|
||||
allOf:
|
||||
- "$ref": "./link.yml"
|
||||
- description: |-
|
||||
Form endpoint that aids in preparing and performing edits on the version
|
||||
|
||||
# Conditions
|
||||
|
||||
**Permission**: manage versions
|
||||
readOnly: true
|
||||
- $ref: "./link.yml"
|
||||
- description: |-
|
||||
Form endpoint that aids in preparing and performing edits on the version
|
||||
|
||||
# Conditions
|
||||
|
||||
**Permission**: manage versions
|
||||
updateImmediately:
|
||||
allOf:
|
||||
- "$ref": "./link.yml"
|
||||
- description: |-
|
||||
Directly perform edits on the version
|
||||
|
||||
# Conditions
|
||||
|
||||
**Permission**: manage versions
|
||||
readOnly: true
|
||||
- $ref: "./link.yml"
|
||||
- description: |-
|
||||
Directly perform edits on the version
|
||||
|
||||
# Conditions
|
||||
|
||||
**Permission**: manage versions
|
||||
self:
|
||||
allOf:
|
||||
- "$ref": "./link.yml"
|
||||
- description: |-
|
||||
This version
|
||||
|
||||
**Resource**: Version
|
||||
readOnly: true
|
||||
- $ref: "./link.yml"
|
||||
- description: |-
|
||||
This version
|
||||
|
||||
**Resource**: Version
|
||||
definingProject:
|
||||
allOf:
|
||||
- "$ref": "./link.yml"
|
||||
- description: |-
|
||||
The workspace to which the version belongs
|
||||
|
||||
**Resource**: Workspace
|
||||
readOnly: true
|
||||
- $ref: "./link.yml"
|
||||
- description: |-
|
||||
The workspace to which the version belongs
|
||||
|
||||
**Resource**: Workspace
|
||||
availableInProjects:
|
||||
allOf:
|
||||
- "$ref": "./link.yml"
|
||||
- description: |-
|
||||
Workspaces where this version can be used
|
||||
|
||||
**Resource**: Workspace
|
||||
readOnly: true
|
||||
- $ref: "./link.yml"
|
||||
- description: |-
|
||||
Workspaces where this version can be used
|
||||
|
||||
**Resource**: Workspace
|
||||
example:
|
||||
_links:
|
||||
self:
|
||||
|
||||
@@ -9,7 +9,7 @@ get:
|
||||
|
||||
+ sharing: filters versions by how they are shared within the server (*none*, *descendants*, *hierarchy*, *tree*, *system*).
|
||||
+ name: filters versions by their name.
|
||||
example: '[{ "sharing": { "operator": "*", "values": ["system"] }" }]'
|
||||
example: '[{ "sharing": { "operator": "=", "values": ["system"] } }]'
|
||||
in: query
|
||||
name: filters
|
||||
required: false
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
# 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::SearchVersions, 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(:admin) } # using an admin, so that versions are visible
|
||||
let(:request_body) do
|
||||
{
|
||||
jsonrpc: "2.0",
|
||||
id: "Test-Request",
|
||||
method: "tools/call",
|
||||
params: {
|
||||
name: "search_versions",
|
||||
arguments: call_args
|
||||
}
|
||||
}
|
||||
end
|
||||
let(:call_args) { {} }
|
||||
let(:parsed_results) { JSON.parse(last_response.body).fetch("result") }
|
||||
|
||||
let(:project) { create(:project) }
|
||||
|
||||
let!(:version_not_shared) { create(:version, project:, sharing: :none, name: "v1.0.1-alpha") }
|
||||
let!(:version_shared_globally) { create(:version, project:, sharing: :system, name: "v1.1.0") }
|
||||
|
||||
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 versions without filters" do
|
||||
subject
|
||||
expect(parsed_results.dig("structuredContent", "items").size).to eq(2)
|
||||
end
|
||||
|
||||
it "responds with properly formatted versions" do
|
||||
subject
|
||||
parsed_results.dig("structuredContent", "items").each do |version|
|
||||
expect(version.to_json).to match_json_schema.from_docs("version_model")
|
||||
end
|
||||
end
|
||||
|
||||
context "when passing an exact name" do
|
||||
let(:call_args) { { name: "v1.0.1-alpha" } }
|
||||
|
||||
it "finds the version" do
|
||||
subject
|
||||
expect(parsed_results.dig("structuredContent", "items").size).to eq(1)
|
||||
end
|
||||
end
|
||||
|
||||
context "when passing a non-exact name" do
|
||||
let(:call_args) { { name: "alpha" } }
|
||||
|
||||
it "finds the version" do
|
||||
subject
|
||||
expect(parsed_results.dig("structuredContent", "items").size).to eq(1)
|
||||
end
|
||||
end
|
||||
|
||||
context "when passing a version sharing strategy" do
|
||||
let(:call_args) { { sharing: "system" } }
|
||||
|
||||
it "finds the version" do
|
||||
subject
|
||||
expect(parsed_results.dig("structuredContent", "items").size).to eq(1)
|
||||
end
|
||||
|
||||
context "and when passing a version name" do
|
||||
let(:call_args) { { sharing: "system", name: "v1" } }
|
||||
|
||||
it "finds the version" do
|
||||
subject
|
||||
expect(parsed_results.dig("structuredContent", "items").size).to eq(1)
|
||||
end
|
||||
end
|
||||
|
||||
context "and when passing a version name of a version with a different sharing strategy" do
|
||||
let(:call_args) { { sharing: "system", name: "alpha" } }
|
||||
|
||||
it "does not find the version" do
|
||||
subject
|
||||
expect(parsed_results.dig("structuredContent", "items")).to be_empty
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "pagination" do
|
||||
let(:page_size) { 10 }
|
||||
let(:overspilling_versions) { 5 }
|
||||
let(:version_count) { page_size + overspilling_versions }
|
||||
let(:call_args) { { name: "beta" } }
|
||||
|
||||
before do
|
||||
allow(described_class).to receive(:page_size).and_return(page_size)
|
||||
|
||||
version_count.times do |idx|
|
||||
create(:version, sharing: :none, name: "v1.2.#{idx}-beta")
|
||||
end
|
||||
end
|
||||
|
||||
it "returns only results up to the page size" do
|
||||
subject
|
||||
expect(parsed_results.dig("structuredContent", "items").count).to eq(page_size)
|
||||
end
|
||||
|
||||
context "if another page is requested" do
|
||||
let(:call_args) { { name: "beta", page: 2 } }
|
||||
|
||||
it "returns the requested page" do
|
||||
subject
|
||||
expect(parsed_results.dig("structuredContent", "items").count).to eq(overspilling_versions)
|
||||
end
|
||||
end
|
||||
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