mcp_lite 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,70 @@
1
+ module McpLite
2
+ module Executor
3
+ class ResourceReader
4
+ def initialize(schema:, context: {})
5
+ @schema = schema
6
+ @context = context
7
+ end
8
+
9
+ def read(uri:)
10
+ unless uri
11
+ return {
12
+ isError: true,
13
+ contents: []
14
+ }
15
+ end
16
+
17
+ resource = @schema.visible_resources.find do |r|
18
+ r.uri == uri
19
+ end
20
+
21
+ unless resource
22
+ return {
23
+ isError: true,
24
+ contents: []
25
+ }
26
+ end
27
+
28
+ if resource.class.respond_to?(:visible?) && !resource.class.visible?(context: @context)
29
+ return {
30
+ isError: true,
31
+ contents: []
32
+ }
33
+ end
34
+
35
+ begin
36
+ if resource.respond_to?(:text) && (content = resource.content)
37
+ {
38
+ contents: [
39
+ {
40
+ uri:,
41
+ mimeType: resource.class.mime_type_value,
42
+ text: content
43
+ }
44
+ ]
45
+ }
46
+ elsif (content = resource.blob)
47
+ {
48
+ contents: [
49
+ {
50
+ uri:,
51
+ mimeType: resource.class.mime_type_value,
52
+ blob: Base64.strict_encode64(content)
53
+ }
54
+ ]
55
+ }
56
+ end
57
+ rescue
58
+ {
59
+ isError: true,
60
+ contents: []
61
+ }
62
+ end
63
+ end
64
+
65
+ private
66
+
67
+ attr_reader :schema, :context
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,102 @@
1
+ module McpLite
2
+ module Executor
3
+ class ToolExecutor
4
+ def initialize(schema:, context: {})
5
+ @schema = schema
6
+ @context = context
7
+ end
8
+
9
+ def execute(name:, arguments: nil)
10
+ unless name
11
+ return {
12
+ isError: true,
13
+ content: [
14
+ {
15
+ type: "text",
16
+ text: "Invalid params: missing tool name"
17
+ }
18
+ ]
19
+ }
20
+ end
21
+
22
+ tool = schema.visible_tools&.find do |tc|
23
+ tc.class.tool_name_value == name
24
+ end
25
+
26
+ unless tool
27
+ return {
28
+ isError: true,
29
+ content: [
30
+ {
31
+ type: "text",
32
+ text: "Tool not found: #{name}"
33
+ }
34
+ ]
35
+ }
36
+ end
37
+
38
+ unless tool.class.visible?(context:)
39
+ return {
40
+ isError: true,
41
+ content: [
42
+ {
43
+ type: "text",
44
+ text: "Unauthorized: Access to tool '#{name}' denied"
45
+ }
46
+ ]
47
+ }
48
+ end
49
+
50
+ args = if arguments.is_a?(String)
51
+ JSON.parse(arguments, symbolize_names: true)
52
+ elsif arguments.respond_to?(:to_hash)
53
+ arguments.to_hash.transform_keys(&:to_sym)
54
+ else
55
+ {}
56
+ end
57
+
58
+ args = args.transform_values do |value|
59
+ if !value.is_a?(String)
60
+ value
61
+ else
62
+ /^\d+$/.match?(value) ? value.to_i : value
63
+ end
64
+ end
65
+
66
+ validation_result = tool.validate(args, @context)
67
+
68
+ if validation_result.is_a?(Hash) && validation_result[:error]
69
+ return {
70
+ isError: true,
71
+ content: [
72
+ {
73
+ type: "text",
74
+ text: validation_result[:error]
75
+ }
76
+ ]
77
+ }
78
+ end
79
+
80
+ begin
81
+ {
82
+ content: tool.call(**args, context:)
83
+ }
84
+ rescue => e
85
+ {
86
+ isError: true,
87
+ content: [
88
+ {
89
+ type: "text",
90
+ text: "Error: #{e.message}"
91
+ }
92
+ ]
93
+ }
94
+ end
95
+ end
96
+
97
+ private
98
+
99
+ attr_reader :schema, :context
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,22 @@
1
+ module McpLite
2
+ module Message
3
+ class Audio
4
+ def initialize(role:, data:, mime_type:)
5
+ @role = role
6
+ @data = data
7
+ @mime_type = mime_type
8
+ end
9
+
10
+ def to_h
11
+ {
12
+ role: @role,
13
+ content: {
14
+ type: "audio",
15
+ data: Base64.strict_encode64(@data),
16
+ mimeType: @mime_type
17
+ }
18
+ }
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,24 @@
1
+ require 'base64'
2
+
3
+ module McpLite
4
+ module Message
5
+ class Image
6
+ def initialize(role:, data:, mime_type:)
7
+ @role = role
8
+ @data = data
9
+ @mime_type = mime_type
10
+ end
11
+
12
+ def to_h
13
+ {
14
+ role: @role,
15
+ content: {
16
+ type: "image",
17
+ data: Base64.strict_encode64(@data),
18
+ mimeType: @mime_type
19
+ }
20
+ }
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,24 @@
1
+ module McpLite
2
+ module Message
3
+ class Resource
4
+ def initialize(role:, resource:)
5
+ @role = role
6
+ @resource = resource
7
+ end
8
+
9
+ def to_h
10
+ {
11
+ role: @role,
12
+ content: {
13
+ type: "resource",
14
+ resource: {
15
+ uri: @resource.uri,
16
+ mimeType: @resource.class.mime_type_value,
17
+ text: @resource.content
18
+ }
19
+ }
20
+ }
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,20 @@
1
+ module McpLite
2
+ module Message
3
+ class Text
4
+ def initialize(role:, text:)
5
+ @role = role
6
+ @text = text
7
+ end
8
+
9
+ def to_h
10
+ {
11
+ role: @role,
12
+ content: {
13
+ type: "text",
14
+ text: @text
15
+ }
16
+ }
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,52 @@
1
+ module McpLite
2
+ module Prompt
3
+ class Base
4
+ class << self
5
+ attr_reader :prompt_name_value, :description_value, :arguments
6
+
7
+ def prompt_name(value)
8
+ @prompt_name_value = value
9
+ end
10
+
11
+ def description(value)
12
+ @description_value = value
13
+ end
14
+
15
+ def argument(name, required: false, description: "", complete: -> {})
16
+ @arguments ||= []
17
+
18
+ @arguments << {
19
+ name:,
20
+ description:,
21
+ required:,
22
+ complete:
23
+ }
24
+ end
25
+ end
26
+
27
+ def initialize(*args, context: {})
28
+ @context = context
29
+ end
30
+
31
+ def prompt_name_value
32
+ self.class.prompt_name_value
33
+ end
34
+
35
+ def description_value
36
+ self.class.description_value
37
+ end
38
+
39
+ def arguments
40
+ self.class.arguments
41
+ end
42
+
43
+ def visible?(context: {})
44
+ true
45
+ end
46
+
47
+ def messages(**args)
48
+ raise NotImplementedError, "#{self.class.name}#messages must be implemented"
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,79 @@
1
+ require "json-schema"
2
+
3
+ module McpLite
4
+ module Resource
5
+ class Base
6
+ class << self
7
+ attr_reader :resource_template_name_value, :description_value, :mime_type_value, :uri_template_value, :schema, :arguments
8
+
9
+ def resource_template_name(value)
10
+ @resource_template_name_value = value
11
+ end
12
+
13
+ def uri_template(value)
14
+ @uri_template_value = value
15
+ end
16
+
17
+ def description(value)
18
+ @description_value = value
19
+ end
20
+
21
+ def mime_type(value)
22
+ @mime_type_value = value
23
+ end
24
+
25
+ def argument(name, complete:)
26
+ @arguments = {}
27
+ @arguments[name] = complete
28
+ end
29
+
30
+ def visible?(context: {})
31
+ true
32
+ end
33
+ end
34
+
35
+ def initialize
36
+ end
37
+
38
+ def visible?(context: {})
39
+ true
40
+ end
41
+
42
+ def resource_template_name_value
43
+ self.class.resource_template_name_value
44
+ end
45
+
46
+ def description_value
47
+ self.class.description_value
48
+ end
49
+
50
+ def mime_type_value
51
+ self.class.mime_type_value
52
+ end
53
+
54
+ def uri_template_value
55
+ self.class.uri_template_value
56
+ end
57
+
58
+ def resource_name
59
+ end
60
+
61
+ def uri
62
+ end
63
+
64
+ def description
65
+ end
66
+
67
+ def content
68
+ case text
69
+ when String
70
+ text
71
+ when Hash
72
+ text.to_json
73
+ else
74
+ text.to_s
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,141 @@
1
+ require_relative "method"
2
+ require_relative "../executor/resource_reader"
3
+ require_relative "../executor/tool_executor"
4
+
5
+ module McpLite
6
+ module Schema
7
+ class Base
8
+ class << self
9
+ attr_reader :tools, :resources, :prompts, :context
10
+
11
+ def tool(value)
12
+ @tools ||= []
13
+ @tools << value
14
+ end
15
+
16
+ def resource(value, items: [])
17
+ @resources ||= []
18
+ @resources << {klass: value, items:}
19
+ end
20
+
21
+ def prompt(value)
22
+ @prompts ||= []
23
+ @prompts << value
24
+ end
25
+
26
+ def execute(params:, context: {})
27
+ @context = context
28
+
29
+ case params[:method]
30
+ when Method::INITIALIZE
31
+ {
32
+ result: {
33
+ protocolVersion: "0.1.0",
34
+ capabilities: {
35
+ resources: {},
36
+ tools: {},
37
+ prompts: {}
38
+ },
39
+ serverInfo: {
40
+ name: "McpLite",
41
+ version: "1.0.0"
42
+ }
43
+ }
44
+ }
45
+ when Method::INITIALIZED
46
+ {result: {}}
47
+ when Method::CANCELLED
48
+ {result: {}}
49
+ when Method::RESOURCES_LIST
50
+ resources = visible_resources.map { |r| {name: r.resource_name, uri: r.uri, description: r.description, mimeType: r.class.mime_type_value } }
51
+ {result: {resources: resources}}
52
+ when Method::RESOURCES_TEMPLATES_LIST
53
+ templates = visible_resource_templates.map { |r| {uriTemplate: r.uri_template_value, name: r.resource_template_name_value} }
54
+ {result: {resourceTemplates: templates}}
55
+ when Method::RESOURCES_READ
56
+ uri = params.dig(:params, :uri)
57
+ resource = resource_reader.read(uri:)
58
+ {result: resource}
59
+ when Method::TOOLS_LIST
60
+ tools = visible_tools.map do |tool|
61
+ {
62
+ name: tool.class.tool_name_value,
63
+ description: tool.class.description_value,
64
+ inputSchema: tool.class.render_schema(context),
65
+ }
66
+ end
67
+ {result: {tools: tools}}
68
+ when Method::TOOLS_CALL
69
+ name = params.dig(:params, :name)
70
+ arguments = params.dig(:params, :arguments)
71
+ unless arguments.is_a?(Hash)
72
+ arguments = arguments.permit!.to_hash.symbolize_keys
73
+ end
74
+ result = tool_executor.execute(name:, arguments:)
75
+ {result:}
76
+ when Method::COMPLETION_COMPLETE
77
+ type = params.dig(:params, :ref, :type)
78
+ completion = McpLite::Completion.new.complete(
79
+ params: params[:params],
80
+ context:,
81
+ refs: (type == "ref/resource") ? visible_resource_templates : visible_prompts
82
+ )
83
+ {result: {completion: completion}}
84
+ when Method::PROMPTS_LIST
85
+ prompts = visible_prompts.map do |prompt|
86
+ {
87
+ name: prompt.prompt_name_value,
88
+ description: prompt.description_value,
89
+ arguments: prompt.arguments.map { _1.except(:complete) }
90
+ }
91
+ end
92
+ {result: {prompts: prompts}}
93
+ when Method::PROMPTS_GET
94
+ prompt = visible_prompts&.find { _1.prompt_name_value == params[:params][:name] }
95
+ arguments = params.dig(:params, :arguments)
96
+ unless arguments.is_a?(Hash)
97
+ arguments = arguments.permit!.to_h.symbolize_keys
98
+ end
99
+ messages = prompt.new.messages(**arguments)
100
+ {result: {messages: messages.map(&:to_h)}}
101
+ else
102
+ {result: {}}
103
+ い end
104
+ end
105
+
106
+ def visible_resources
107
+ resources&.filter do |resource|
108
+ resource[:klass].visible?(context: @context)
109
+ end&.map do |resource|
110
+ resource[:items].map do |item|
111
+ resource[:klass].new(**item)
112
+ end
113
+ end&.flatten || []
114
+ end
115
+
116
+ def visible_resource_templates
117
+ visibles = resources&.filter do |resource|
118
+ resource[:klass].uri_template_value && resource[:klass].visible?(context: @context)
119
+ end
120
+ visibles&.map { _1[:klass] } || []
121
+ end
122
+
123
+ def visible_tools
124
+ tools&.map(&:new) || []
125
+ end
126
+
127
+ def visible_prompts
128
+ prompts || []
129
+ end
130
+
131
+ def resource_reader
132
+ @resource_reader ||= Executor::ResourceReader.new(schema: self, context:)
133
+ end
134
+
135
+ def tool_executor
136
+ @tool_executor ||= Executor::ToolExecutor.new(schema: self, context:)
137
+ end
138
+ end
139
+ end
140
+ end
141
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module McpLite
4
+ module Schema
5
+ module Method
6
+ INITIALIZE = "initialize"
7
+ INITIALIZED = "notifications/initialized"
8
+ CANCELLED = "notifications/cancelled"
9
+ PING = "ping"
10
+ TOOLS_LIST = "tools/list"
11
+ TOOLS_CALL = "tools/call"
12
+ RESOURCES_LIST = "resources/list"
13
+ RESOURCES_READ = "resources/read"
14
+ RESOURCES_TEMPLATES_LIST = "resources/templates/list"
15
+ COMPLETION_COMPLETE = "completion/complete"
16
+ PROMPTS_LIST = "prompts/list"
17
+ PROMPTS_GET = "prompts/get"
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module McpLite
4
+ module ErrorCode
5
+ NOT_INITIALIZED = -32_002
6
+ ALREADY_INITIALIZED = -32_002
7
+
8
+ PARSE_ERROR = -32_700
9
+ INVALID_REQUEST = -32_600
10
+ METHOD_NOT_FOUND = -32_601
11
+ INVALID_PARAMS = -32_602
12
+ INTERNAL_ERROR = -32_603
13
+ end
14
+ end
@@ -0,0 +1,55 @@
1
+ module McpLite
2
+ class Server
3
+ class Fetcher
4
+ def initialize(base_uri: nil, headers: {})
5
+ @base_uri = base_uri
6
+
7
+ if headers.is_a?(Hash)
8
+ @headers = headers
9
+ end
10
+ end
11
+
12
+ def call(params:)
13
+ return unless @base_uri
14
+
15
+ require "net/http"
16
+
17
+ unless @base_uri.is_a?(URI) || @base_uri.is_a?(String)
18
+ Server.log_error("Invalid URI type", StandardError.new("URI must be a String or URI object"))
19
+ return
20
+ end
21
+
22
+ begin
23
+ uri = URI.parse(@base_uri.to_s)
24
+
25
+ unless uri.scheme =~ /\Ahttps?\z/ && !uri.host.nil?
26
+ Server.log_error("Invalid URI", StandardError.new("URI must have a valid scheme and host"))
27
+ return
28
+ end
29
+ rescue URI::InvalidURIError => e
30
+ Server.log_error("Invalid URI format", e)
31
+ return
32
+ end
33
+
34
+ request = Net::HTTP::Post.new(uri)
35
+ request.body = JSON.generate(params)
36
+ request["Content-Type"] = "application/json"
37
+ request["Accept"] = "application/json"
38
+ @headers.each do |key, value|
39
+ request[key] = value
40
+ end
41
+
42
+ begin
43
+ response = Net::HTTP.start(uri.hostname, uri.port) do |http|
44
+ http.request(request)
45
+ end
46
+
47
+ JSON.parse(response.body, symbolize_names: true)
48
+ rescue => e
49
+ Server.log_error("Error fetching resource_templates", e)
50
+ nil
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module McpLite
4
+ module Method
5
+ INITIALIZE = "initialize"
6
+ INITIALIZED = "notifications/initialized"
7
+ CANCELLED = "notifications/cancelled"
8
+ PING = "ping"
9
+ TOOLS_LIST = "tools/list"
10
+ TOOLS_CALL = "tools/call"
11
+ RESOURCES_LIST = "resources/list"
12
+ RESOURCES_READ = "resources/read"
13
+ RESOURCES_TEMPLATES_LIST = "resources/templates/list"
14
+ COMPLETION_COMPLETE = "completion/complete"
15
+ PROMPTS_LIST = "prompts/list"
16
+ PROMPTS_GET = "prompts/get"
17
+ end
18
+ end