Internal Release 0.7.0
- add support for resource templates - improve capability negotiation on handlers - clean up readme - reduce default protocol version to "2024-11-05" - address spec compliance of resources - add debug gem to development - alias ModelContextProtocol to MCP - move prompt subclasses to subdir - improve tool and prompt definition - improve passing of server context to tools - improve tool input schema validation
This commit is contained in:
parent
62184957e0
commit
baeb99ffbc
15 changed files with 842 additions and 253 deletions
|
|
@ -4,9 +4,18 @@
|
|||
require "model_context_protocol/server"
|
||||
require "model_context_protocol/string_utils"
|
||||
require "model_context_protocol/tool"
|
||||
require "model_context_protocol/tool/input_schema"
|
||||
require "model_context_protocol/tool/annotations"
|
||||
require "model_context_protocol/tool/response"
|
||||
require "model_context_protocol/content"
|
||||
require "model_context_protocol/resource"
|
||||
require "model_context_protocol/resource/contents"
|
||||
require "model_context_protocol/resource/embedded"
|
||||
require "model_context_protocol/resource_template"
|
||||
require "model_context_protocol/prompt"
|
||||
require "model_context_protocol/prompt/argument"
|
||||
require "model_context_protocol/prompt/message"
|
||||
require "model_context_protocol/prompt/result"
|
||||
require "model_context_protocol/version"
|
||||
require "model_context_protocol/configuration"
|
||||
require "model_context_protocol/methods"
|
||||
|
|
@ -31,3 +40,5 @@ module ModelContextProtocol
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
MCP = ModelContextProtocol
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
module ModelContextProtocol
|
||||
class Configuration
|
||||
DEFAULT_PROTOCOL_VERSION = "2025-03-26"
|
||||
DEFAULT_PROTOCOL_VERSION = "2024-11-05"
|
||||
|
||||
attr_writer :exception_reporter, :instrumentation_callback, :protocol_version
|
||||
|
||||
|
|
|
|||
|
|
@ -2,13 +2,85 @@
|
|||
|
||||
module ModelContextProtocol
|
||||
module Methods
|
||||
RESOURCES_LIST = "resources/list"
|
||||
RESOURCES_READ = "resources/read"
|
||||
TOOLS_LIST = "tools/list"
|
||||
TOOLS_CALL = "tools/call"
|
||||
PROMPTS_LIST = "prompts/list"
|
||||
PROMPTS_GET = "prompts/get"
|
||||
INITIALIZE = "initialize"
|
||||
PING = "ping"
|
||||
LOGGING_SET_LEVEL = "logging/setLevel"
|
||||
|
||||
PROMPTS_GET = "prompts/get"
|
||||
PROMPTS_LIST = "prompts/list"
|
||||
COMPLETION_COMPLETE = "completion/complete"
|
||||
|
||||
RESOURCES_LIST = "resources/list"
|
||||
RESOURCES_READ = "resources/read"
|
||||
RESOURCES_TEMPLATES_LIST = "resources/templates/list"
|
||||
RESOURCES_SUBSCRIBE = "resources/subscribe"
|
||||
RESOURCES_UNSUBSCRIBE = "resources/unsubscribe"
|
||||
|
||||
TOOLS_CALL = "tools/call"
|
||||
TOOLS_LIST = "tools/list"
|
||||
|
||||
SAMPLING_CREATE_MESSAGE = "sampling/createMessage"
|
||||
|
||||
class MissingRequiredCapabilityError < StandardError
|
||||
attr_reader :method
|
||||
attr_reader :capability
|
||||
|
||||
def initialize(method, capability)
|
||||
super("Server does not support #{capability} (required for #{method})")
|
||||
@method = method
|
||||
@capability = capability
|
||||
end
|
||||
end
|
||||
|
||||
extend self
|
||||
|
||||
ALL_METHODS = [
|
||||
SAMPLING_CREATE_MESSAGE,
|
||||
INITIALIZE,
|
||||
LOGGING_SET_LEVEL,
|
||||
PING,
|
||||
PROMPTS_GET,
|
||||
PROMPTS_LIST,
|
||||
COMPLETION_COMPLETE,
|
||||
RESOURCES_LIST,
|
||||
RESOURCES_READ,
|
||||
RESOURCES_TEMPLATES_LIST,
|
||||
RESOURCES_SUBSCRIBE,
|
||||
RESOURCES_UNSUBSCRIBE,
|
||||
TOOLS_CALL,
|
||||
TOOLS_LIST,
|
||||
]
|
||||
|
||||
def ensure_capability!(method, capabilities)
|
||||
case method
|
||||
when PROMPTS_GET, PROMPTS_LIST, COMPLETION_COMPLETE
|
||||
unless capabilities[:prompts]
|
||||
raise MissingRequiredCapabilityError.new(method, :prompts)
|
||||
end
|
||||
when RESOURCES_LIST, RESOURCES_TEMPLATES_LIST, RESOURCES_READ, RESOURCES_SUBSCRIBE, RESOURCES_UNSUBSCRIBE
|
||||
unless capabilities[:resources]
|
||||
raise MissingRequiredCapabilityError.new(method, :resources)
|
||||
end
|
||||
|
||||
if method == RESOURCES_SUBSCRIBE && !capabilities[:resources][:subscribe]
|
||||
raise MissingRequiredCapabilityError.new(method, :resources_subscribe)
|
||||
end
|
||||
when TOOLS_CALL, TOOLS_LIST
|
||||
unless capabilities[:tools]
|
||||
raise MissingRequiredCapabilityError.new(method, :tools)
|
||||
end
|
||||
when SAMPLING_CREATE_MESSAGE
|
||||
unless capabilities[:sampling]
|
||||
raise MissingRequiredCapabilityError.new(method, :sampling)
|
||||
end
|
||||
when LOGGING_SET_LEVEL
|
||||
# Logging is unsupported by the Server
|
||||
unless capabilities[:logging]
|
||||
raise MissingRequiredCapabilityError.new(method, :logging)
|
||||
end
|
||||
when INITIALIZE, PING
|
||||
# No specific capability required for initialize or ping
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -3,47 +3,6 @@
|
|||
|
||||
module ModelContextProtocol
|
||||
class Prompt
|
||||
class Argument
|
||||
attr_reader :name, :description, :required, :arguments
|
||||
|
||||
def initialize(name:, description: nil, required: false)
|
||||
@name = name
|
||||
@description = description
|
||||
@required = required
|
||||
@arguments = arguments
|
||||
end
|
||||
|
||||
def to_h
|
||||
{ name:, description:, required: }.compact
|
||||
end
|
||||
end
|
||||
|
||||
class Message
|
||||
attr_reader :role, :content
|
||||
|
||||
def initialize(role:, content:)
|
||||
@role = role
|
||||
@content = content
|
||||
end
|
||||
|
||||
def to_h
|
||||
{ role:, content: content.to_h }.compact
|
||||
end
|
||||
end
|
||||
|
||||
class Result
|
||||
attr_reader :description, :messages
|
||||
|
||||
def initialize(description: nil, messages: [])
|
||||
@description = description
|
||||
@messages = messages
|
||||
end
|
||||
|
||||
def to_h
|
||||
{ description:, messages: messages.map(&:to_h) }.compact
|
||||
end
|
||||
end
|
||||
|
||||
class << self
|
||||
NOT_SET = Object.new
|
||||
|
||||
|
|
|
|||
21
lib/model_context_protocol/prompt/argument.rb
Normal file
21
lib/model_context_protocol/prompt/argument.rb
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
# typed: strict
|
||||
# frozen_string_literal: true
|
||||
|
||||
module ModelContextProtocol
|
||||
class Prompt
|
||||
class Argument
|
||||
attr_reader :name, :description, :required, :arguments
|
||||
|
||||
def initialize(name:, description: nil, required: false)
|
||||
@name = name
|
||||
@description = description
|
||||
@required = required
|
||||
@arguments = arguments
|
||||
end
|
||||
|
||||
def to_h
|
||||
{ name:, description:, required: }.compact
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
19
lib/model_context_protocol/prompt/message.rb
Normal file
19
lib/model_context_protocol/prompt/message.rb
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
# typed: strict
|
||||
# frozen_string_literal: true
|
||||
|
||||
module ModelContextProtocol
|
||||
class Prompt
|
||||
class Message
|
||||
attr_reader :role, :content
|
||||
|
||||
def initialize(role:, content:)
|
||||
@role = role
|
||||
@content = content
|
||||
end
|
||||
|
||||
def to_h
|
||||
{ role:, content: content.to_h }.compact
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
19
lib/model_context_protocol/prompt/result.rb
Normal file
19
lib/model_context_protocol/prompt/result.rb
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
# typed: strict
|
||||
# frozen_string_literal: true
|
||||
|
||||
module ModelContextProtocol
|
||||
class Prompt
|
||||
class Result
|
||||
attr_reader :description, :messages
|
||||
|
||||
def initialize(description: nil, messages: [])
|
||||
@description = description
|
||||
@messages = messages
|
||||
end
|
||||
|
||||
def to_h
|
||||
{ description:, messages: messages.map(&:to_h) }.compact
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -3,14 +3,13 @@
|
|||
|
||||
module ModelContextProtocol
|
||||
class Resource
|
||||
attr_reader :uri, :name, :description, :mime_type, :contents
|
||||
attr_reader :uri, :name, :description, :mime_type
|
||||
|
||||
def initialize(uri:, name:, description:, mime_type:, contents:)
|
||||
def initialize(uri:, name:, description:, mime_type:)
|
||||
@uri = uri
|
||||
@name = name
|
||||
@description = description
|
||||
@mime_type = mime_type
|
||||
@contents = contents
|
||||
end
|
||||
|
||||
def to_h
|
||||
|
|
@ -19,7 +18,6 @@ module ModelContextProtocol
|
|||
name: @name,
|
||||
description: @description,
|
||||
mimeType: @mime_type,
|
||||
contents: @contents.map(&:to_h),
|
||||
}
|
||||
end
|
||||
end
|
||||
|
|
|
|||
24
lib/model_context_protocol/resource_template.rb
Normal file
24
lib/model_context_protocol/resource_template.rb
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
# typed: strict
|
||||
# frozen_string_literal: true
|
||||
|
||||
module ModelContextProtocol
|
||||
class ResourceTemplate
|
||||
attr_reader :uri_template, :name, :description, :mime_type
|
||||
|
||||
def initialize(uri_template:, name:, description: nil, mime_type: nil)
|
||||
@uri_template = uri_template
|
||||
@name = name
|
||||
@description = description
|
||||
@mime_type = mime_type
|
||||
end
|
||||
|
||||
def to_h
|
||||
{
|
||||
uriTemplate: @uri_template,
|
||||
name: @name,
|
||||
description: @description,
|
||||
mimeType: @mime_type,
|
||||
}.compact
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -20,26 +20,43 @@ module ModelContextProtocol
|
|||
|
||||
include Instrumentation
|
||||
|
||||
attr_accessor :name, :tools, :prompts, :resources, :server_context, :configuration
|
||||
attr_accessor :name, :tools, :prompts, :resources, :server_context, :configuration, :capabilities
|
||||
|
||||
def initialize(name: "model_context_protocol", tools: [], prompts: [], resources: [], server_context: nil,
|
||||
configuration: nil)
|
||||
def initialize(
|
||||
name: "model_context_protocol",
|
||||
tools: [],
|
||||
prompts: [],
|
||||
resources: [],
|
||||
resource_templates: [],
|
||||
server_context: nil,
|
||||
configuration: nil,
|
||||
capabilities: { prompts: {}, resources: {}, tools: {} }
|
||||
)
|
||||
@name = name
|
||||
@tools = tools.to_h { |t| [t.name_value, t] }
|
||||
@prompts = prompts.to_h { |p| [p.name_value, p] }
|
||||
@resources = resources
|
||||
@resource_templates = resource_templates
|
||||
@resource_index = index_resources_by_uri(resources)
|
||||
@server_context = server_context
|
||||
@configuration = ModelContextProtocol.configuration.merge(configuration)
|
||||
@capabilities = capabilities
|
||||
|
||||
@handlers = {
|
||||
Methods::RESOURCES_LIST => method(:list_resources),
|
||||
Methods::RESOURCES_READ => method(:read_resource),
|
||||
Methods::RESOURCES_READ => method(:read_resource_no_content),
|
||||
Methods::RESOURCES_TEMPLATES_LIST => method(:list_resource_templates),
|
||||
Methods::TOOLS_LIST => method(:list_tools),
|
||||
Methods::TOOLS_CALL => method(:call_tool),
|
||||
Methods::PROMPTS_LIST => method(:list_prompts),
|
||||
Methods::PROMPTS_GET => method(:get_prompt),
|
||||
Methods::INITIALIZE => method(:init),
|
||||
Methods::PING => ->(_) { {} },
|
||||
|
||||
# No op handlers for currently unsupported methods
|
||||
Methods::RESOURCES_SUBSCRIBE => ->(_) {},
|
||||
Methods::RESOURCES_UNSUBSCRIBE => ->(_) {},
|
||||
Methods::LOGGING_SET_LEVEL => ->(_) {},
|
||||
}
|
||||
end
|
||||
|
||||
|
|
@ -55,6 +72,16 @@ module ModelContextProtocol
|
|||
end
|
||||
end
|
||||
|
||||
def define_tool(name: nil, description: nil, input_schema: nil, annotations: nil, &block)
|
||||
tool = Tool.define(name:, description:, input_schema:, annotations:, &block)
|
||||
@tools[tool.name_value] = tool
|
||||
end
|
||||
|
||||
def define_prompt(name: nil, description: nil, arguments: [], &block)
|
||||
prompt = Prompt.define(name:, description:, arguments:, &block)
|
||||
@prompts[prompt.name_value] = prompt
|
||||
end
|
||||
|
||||
def resources_list_handler(&block)
|
||||
@handlers[Methods::RESOURCES_LIST] = block
|
||||
end
|
||||
|
|
@ -63,6 +90,10 @@ module ModelContextProtocol
|
|||
@handlers[Methods::RESOURCES_READ] = block
|
||||
end
|
||||
|
||||
def resources_templates_list_handler(&block)
|
||||
@handlers[Methods::RESOURCES_TEMPLATES_LIST] = block
|
||||
end
|
||||
|
||||
def tools_list_handler(&block)
|
||||
@handlers[Methods::TOOLS_LIST] = block
|
||||
end
|
||||
|
|
@ -88,6 +119,8 @@ module ModelContextProtocol
|
|||
return
|
||||
end
|
||||
|
||||
Methods.ensure_capability!(method, capabilities)
|
||||
|
||||
->(params) {
|
||||
instrument_call(method) do
|
||||
case method
|
||||
|
|
@ -97,6 +130,10 @@ module ModelContextProtocol
|
|||
{ prompts: @handlers[Methods::PROMPTS_LIST].call(params) }
|
||||
when Methods::RESOURCES_LIST
|
||||
{ resources: @handlers[Methods::RESOURCES_LIST].call(params) }
|
||||
when Methods::RESOURCES_READ
|
||||
{ contents: @handlers[Methods::RESOURCES_READ].call(params) }
|
||||
when Methods::RESOURCES_TEMPLATES_LIST
|
||||
{ resourceTemplates: @handlers[Methods::RESOURCES_TEMPLATES_LIST].call(params) }
|
||||
else
|
||||
@handlers[method].call(params)
|
||||
end
|
||||
|
|
@ -113,14 +150,6 @@ module ModelContextProtocol
|
|||
}
|
||||
end
|
||||
|
||||
def capabilities
|
||||
@capabilities ||= {
|
||||
prompts: {},
|
||||
resources: {},
|
||||
tools: {},
|
||||
}
|
||||
end
|
||||
|
||||
def server_info
|
||||
@server_info ||= {
|
||||
name:,
|
||||
|
|
@ -151,10 +180,25 @@ module ModelContextProtocol
|
|||
raise RequestHandlerError.new("Tool not found #{tool_name}", request, error_type: :tool_not_found)
|
||||
end
|
||||
|
||||
arguments = request[:arguments]
|
||||
add_instrumentation_data(tool_name:)
|
||||
|
||||
if tool.input_schema&.missing_required_arguments?(arguments)
|
||||
add_instrumentation_data(error: :missing_required_arguments)
|
||||
raise RequestHandlerError.new(
|
||||
"Missing required arguments: #{tool.input_schema.missing_required_arguments(arguments).join(", ")}",
|
||||
request,
|
||||
error_type: :missing_required_arguments,
|
||||
)
|
||||
end
|
||||
|
||||
begin
|
||||
tool.call(**request[:arguments], server_context:).to_h
|
||||
call_params = tool.method(:call).parameters.flatten
|
||||
if call_params.include?(:server_context)
|
||||
tool.call(**arguments.transform_keys(&:to_sym), server_context:).to_h
|
||||
else
|
||||
tool.call(**arguments.transform_keys(&:to_sym)).to_h
|
||||
end
|
||||
rescue => e
|
||||
raise RequestHandlerError.new("Internal error calling tool #{tool_name}", request, original_error: e)
|
||||
end
|
||||
|
|
@ -188,18 +232,17 @@ module ModelContextProtocol
|
|||
@resources.map(&:to_h)
|
||||
end
|
||||
|
||||
def read_resource(request)
|
||||
# Server implementation should set read_resource_handler to override no-op default
|
||||
def read_resource_no_content(request)
|
||||
add_instrumentation_data(method: Methods::RESOURCES_READ)
|
||||
resource_uri = request[:uri]
|
||||
add_instrumentation_data(resource_uri: request[:uri])
|
||||
[]
|
||||
end
|
||||
|
||||
resource = @resource_index[resource_uri]
|
||||
unless resource
|
||||
add_instrumentation_data(error: :resource_not_found)
|
||||
raise RequestHandlerError.new("Resource not found #{resource_uri}", request, error_type: :resource_not_found)
|
||||
end
|
||||
def list_resource_templates(request)
|
||||
add_instrumentation_data(method: Methods::RESOURCES_TEMPLATES_LIST)
|
||||
|
||||
add_instrumentation_data(resource_uri:)
|
||||
resource.to_h
|
||||
@resource_templates.map(&:to_h)
|
||||
end
|
||||
|
||||
def report_exception(exception, server_context = {})
|
||||
|
|
|
|||
|
|
@ -2,41 +2,6 @@
|
|||
|
||||
module ModelContextProtocol
|
||||
class Tool
|
||||
class Response
|
||||
attr_reader :content, :is_error
|
||||
|
||||
def initialize(content, is_error = false)
|
||||
@content = content
|
||||
@is_error = is_error
|
||||
end
|
||||
|
||||
def to_h
|
||||
{ content:, isError: is_error }.compact
|
||||
end
|
||||
end
|
||||
|
||||
class Annotations
|
||||
attr_reader :title, :read_only_hint, :destructive_hint, :idempotent_hint, :open_world_hint
|
||||
|
||||
def initialize(title: nil, read_only_hint: nil, destructive_hint: nil, idempotent_hint: nil, open_world_hint: nil)
|
||||
@title = title
|
||||
@read_only_hint = read_only_hint
|
||||
@destructive_hint = destructive_hint
|
||||
@idempotent_hint = idempotent_hint
|
||||
@open_world_hint = open_world_hint
|
||||
end
|
||||
|
||||
def to_h
|
||||
{
|
||||
title:,
|
||||
readOnlyHint: read_only_hint,
|
||||
destructiveHint: destructive_hint,
|
||||
idempotentHint: idempotent_hint,
|
||||
openWorldHint: open_world_hint,
|
||||
}.compact
|
||||
end
|
||||
end
|
||||
|
||||
class << self
|
||||
NOT_SET = Object.new
|
||||
|
||||
|
|
@ -52,7 +17,7 @@ module ModelContextProtocol
|
|||
result = {
|
||||
name: name_value,
|
||||
description: description_value,
|
||||
inputSchema: input_schema_value,
|
||||
inputSchema: input_schema_value.to_h,
|
||||
}
|
||||
result[:annotations] = annotations_value.to_h if annotations_value
|
||||
result
|
||||
|
|
@ -88,8 +53,12 @@ module ModelContextProtocol
|
|||
|
||||
def input_schema(value = NOT_SET)
|
||||
if value == NOT_SET
|
||||
@input_schema_value
|
||||
else
|
||||
input_schema_value
|
||||
elsif value.is_a?(Hash)
|
||||
properties = value[:properties] || value["properties"] || {}
|
||||
required = value[:required] || value["required"] || []
|
||||
@input_schema_value = InputSchema.new(properties:, required:)
|
||||
elsif value.is_a?(InputSchema)
|
||||
@input_schema_value = value
|
||||
end
|
||||
end
|
||||
|
|
@ -108,9 +77,7 @@ module ModelContextProtocol
|
|||
description description
|
||||
input_schema input_schema
|
||||
self.annotations(annotations) if annotations
|
||||
define_singleton_method(:call) do |*args, server_context:|
|
||||
instance_exec(*args, server_context:, &block)
|
||||
end
|
||||
define_singleton_method(:call, &block) if block
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
27
lib/model_context_protocol/tool/annotations.rb
Normal file
27
lib/model_context_protocol/tool/annotations.rb
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module ModelContextProtocol
|
||||
class Tool
|
||||
class Annotations
|
||||
attr_reader :title, :read_only_hint, :destructive_hint, :idempotent_hint, :open_world_hint
|
||||
|
||||
def initialize(title: nil, read_only_hint: nil, destructive_hint: nil, idempotent_hint: nil, open_world_hint: nil)
|
||||
@title = title
|
||||
@read_only_hint = read_only_hint
|
||||
@destructive_hint = destructive_hint
|
||||
@idempotent_hint = idempotent_hint
|
||||
@open_world_hint = open_world_hint
|
||||
end
|
||||
|
||||
def to_h
|
||||
{
|
||||
title:,
|
||||
readOnlyHint: read_only_hint,
|
||||
destructiveHint: destructive_hint,
|
||||
idempotentHint: idempotent_hint,
|
||||
openWorldHint: open_world_hint,
|
||||
}.compact
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
26
lib/model_context_protocol/tool/input_schema.rb
Normal file
26
lib/model_context_protocol/tool/input_schema.rb
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module ModelContextProtocol
|
||||
class Tool
|
||||
class InputSchema
|
||||
attr_reader :properties, :required
|
||||
|
||||
def initialize(properties: {}, required: [])
|
||||
@properties = properties
|
||||
@required = required.map(&:to_sym)
|
||||
end
|
||||
|
||||
def to_h
|
||||
{ type: "object", properties:, required: }
|
||||
end
|
||||
|
||||
def missing_required_arguments?(arguments)
|
||||
missing_required_arguments(arguments).any?
|
||||
end
|
||||
|
||||
def missing_required_arguments(arguments)
|
||||
(required - arguments.keys.map(&:to_sym))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
18
lib/model_context_protocol/tool/response.rb
Normal file
18
lib/model_context_protocol/tool/response.rb
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module ModelContextProtocol
|
||||
class Tool
|
||||
class Response
|
||||
attr_reader :content, :is_error
|
||||
|
||||
def initialize(content, is_error = false)
|
||||
@content = content
|
||||
@is_error = is_error
|
||||
end
|
||||
|
||||
def to_h
|
||||
{ content:, isError: is_error }.compact
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module ModelContextProtocol
|
||||
VERSION = "0.6.0"
|
||||
VERSION = "0.7.0"
|
||||
end
|
||||
|
|
|
|||
Loading…
Reference in a new issue