Skip to content
Draft
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
1 change: 1 addition & 0 deletions PluginGemfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
source 'https://rubygems.org'

gem 'mcp'
gem 'view_component', '~> 3.8'

group :development, :test do
Expand Down
62 changes: 62 additions & 0 deletions app/controllers/mcp_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# frozen_string_literal: true

require "json"

Check failure on line 3 in app/controllers/mcp_controller.rb

View workflow job for this annotation

GitHub Actions / lint-and-test (6.0-stable)

Style/StringLiterals: Prefer single-quoted strings when you don't need string interpolation or special symbols.

class McpController < ApplicationController
skip_before_action :verify_authenticity_token

# TODO: no auth here!!!!!!
# before_action :authenticate_with_bearer_token

def handle

Check failure on line 11 in app/controllers/mcp_controller.rb

View workflow job for this annotation

GitHub Actions / lint-and-test (6.0-stable)

Metrics/PerceivedComplexity: Perceived complexity for handle is too high. [9/8]

Check failure on line 11 in app/controllers/mcp_controller.rb

View workflow job for this annotation

GitHub Actions / lint-and-test (6.0-stable)

Metrics/MethodLength: Method has too many lines. [18/10]

Check failure on line 11 in app/controllers/mcp_controller.rb

View workflow job for this annotation

GitHub Actions / lint-and-test (6.0-stable)

Metrics/AbcSize: Assignment Branch Condition size for handle is too high. [<12, 28, 8> 31.5/17]
transport_response = mcp_transport.handle_request(request)
status, headers, body = transport_response

headers.each { |key, value| response.headers[key] = value }

if body.respond_to?(:call)
# SSE streaming response
response.headers["Content-Type"] = "text/event-stream"

Check failure on line 19 in app/controllers/mcp_controller.rb

View workflow job for this annotation

GitHub Actions / lint-and-test (6.0-stable)

Style/StringLiterals: Prefer single-quoted strings when you don't need string interpolation or special symbols.

Check failure on line 19 in app/controllers/mcp_controller.rb

View workflow job for this annotation

GitHub Actions / lint-and-test (6.0-stable)

Style/StringLiterals: Prefer single-quoted strings when you don't need string interpolation or special symbols.
response.headers["Cache-Control"] = "no-cache"

Check failure on line 20 in app/controllers/mcp_controller.rb

View workflow job for this annotation

GitHub Actions / lint-and-test (6.0-stable)

Style/StringLiterals: Prefer single-quoted strings when you don't need string interpolation or special symbols.

Check failure on line 20 in app/controllers/mcp_controller.rb

View workflow job for this annotation

GitHub Actions / lint-and-test (6.0-stable)

Style/StringLiterals: Prefer single-quoted strings when you don't need string interpolation or special symbols.
response.headers["Connection"] = "keep-alive"

Check failure on line 21 in app/controllers/mcp_controller.rb

View workflow job for this annotation

GitHub Actions / lint-and-test (6.0-stable)

Style/StringLiterals: Prefer single-quoted strings when you don't need string interpolation or special symbols.

Check failure on line 21 in app/controllers/mcp_controller.rb

View workflow job for this annotation

GitHub Actions / lint-and-test (6.0-stable)

Style/StringLiterals: Prefer single-quoted strings when you don't need string interpolation or special symbols.

self.response_body = body
else
# Handle array of JSON strings (parse first element) or direct JSON object
response_body = if body.is_a?(Array) && body.first.is_a?(String)
JSON.parse(body.first)
elsif body.is_a?(Array) && body.first.is_a?(Hash)
body.first
else
body
end

render json: response_body, status: status
end
end

private

def mcp_transport
@@mcp_transport ||= begin
Dir[Rails.root.join("plugins/redmine_tracky/app/mcp/**/*.rb")].each { |f| require f }

resource_classes = [IssueResource ]

server = MCP::Server.new(
name: "rails-demo-server",
tools: [ CreateTimerSessionTool ],
resources: [IssueResource.resource ],
prompts: []
)

server.resources_read_handler do |request|
uri = request[:uri]
resource_class = resource_classes.find { |klass| klass::URI == uri || klass.matches?(uri) }
resource_class ? resource_class.read(uri) : []
end

MCP::Server::Transports::StreamableHTTPTransport.new(server)
end
end
end
36 changes: 36 additions & 0 deletions app/mcp/resources/issue_resource.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
class IssueResource
URI = "rails://issues"

def self.resource
MCP::Resource.new(
uri: URI,
name: "Issues",
description: "List of all issues available to user",
mime_type: "text/plain"
)
end

def self.read(_uri)
stats_text = generate_issue_list

[
MCP::Resource::TextContents.new(
uri: URI,
text: stats_text,
mime_type: "text/plain"
)
]
end

private

def self.generate_issue_list
<<~STATS
# Issue liste

## Issues
#{Issue.pluck(:id, :subject ).map { |id, subject| "- #{id}: #{subject}" }.join("\n") }
STATS
end

end
87 changes: 87 additions & 0 deletions app/mcp/tools/create_timer_session_tool.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
class CreateTimerSessionTool < MCP::Tool
tool_name "create_timer_session"
description "Book worktime on issues with start and end times"

input_schema(
properties: {
issue_ids: {
type: "array",
items: {
type: "number"
},
minContains: 1,
description: "Collection of issue ids to book time on (distribute equally)"
},
description: {
type: "string",
description: "Summary of the work which has been done for this issue"
},
timer_session_start: {
type: "string",
format: "date-time",
description: "When the work on this issue has been started"
},
timer_session_end: {
type: "string",
format: "date-time",
description: "When the work on this issue stopped"
}
},
required: [ "issue_ids", "description", "timer_session_start", "timer_session_end" ]
)

output_schema(
properties: {
success: {
type: "boolean",
description: "True for success"
}
}
)

class << self
def call(issue_ids:, description:, timer_session_start:, timer_session_end:)
session = TimerSession.new(
timer_start: timer_session_start,
comments: description,
user: User.first,
timer_end: timer_session_end
)

if session.save
session.issues << Issue.find(issue_ids)
success_response(session)
else
error_response("Error creating issue: \n#{session.errors.full_messages.join("\n")}")
end

rescue => e
error_response("Error creating issue: #{e.message}")
end

private

def success_response(session)
response_text = <<~TEXT
Success!
TEXT

structured_content = {
success: true,
}

MCP::Tool::Response.new([ {
type: "text",
text: response_text.strip
} ], structured_content: structured_content)
end

def error_response(message)
puts message
MCP::Tool::Response.new([ {
type: "text",
text: message
} ], error: true)
end
end
end
2 changes: 2 additions & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,5 @@
delete :time_tracker, to: 'time_tracker#destroy'

get 'completion/issues', to: 'completion#issues'

match "/mcp" => "mcp#handle", via: [ :get, :post, :delete ]