micro_mcp 0.1.0 → 0.1.4
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.
- checksums.yaml +4 -4
- data/.devcontainer/devcontainer.json +2 -2
- data/.tool-versions +1 -0
- data/AGENTS.md +5 -1
- data/CHANGELOG.md +12 -0
- data/Cargo.lock +278 -558
- data/README.md +39 -8
- data/docs/changes/ERGONOMIC_IMPROVEMENTS.md +133 -0
- data/ext/micro_mcp/AGENTS.md +66 -0
- data/ext/micro_mcp/Cargo.toml +3 -1
- data/ext/micro_mcp/src/lib.rs +21 -5
- data/ext/micro_mcp/src/server.rs +470 -50
- data/ext/micro_mcp/src/utils.rs +1 -1
- data/lib/micro_mcp/runtime_helpers.rb +104 -0
- data/lib/micro_mcp/schema.rb +125 -0
- data/lib/micro_mcp/server.rb +2 -2
- data/lib/micro_mcp/tool_registry.rb +65 -2
- data/lib/micro_mcp/validation_helpers.rb +59 -0
- data/lib/micro_mcp/version.rb +1 -1
- data/lib/micro_mcp.rb +13 -1
- metadata +9 -4
- data/sig/micro_mcp.rbs +0 -4
@@ -0,0 +1,104 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "validation_helpers"
|
4
|
+
|
5
|
+
module MicroMcp
|
6
|
+
module RuntimeHelpers
|
7
|
+
# Helper method to make create_message calls more ergonomic
|
8
|
+
# Automatically extracts the text content from the response
|
9
|
+
def ask_assistant(question, system_prompt: "You are a helpful assistant.", max_tokens: 100, model_hints: ["o4-mini"])
|
10
|
+
params = {
|
11
|
+
"messages" => [
|
12
|
+
{
|
13
|
+
"role" => "user",
|
14
|
+
"content" => {"type" => "text", "text" => question}
|
15
|
+
}
|
16
|
+
],
|
17
|
+
"modelPreferences" => {
|
18
|
+
"hints" => model_hints.map { |name| {"name" => name} },
|
19
|
+
"intelligencePriority" => 0.8,
|
20
|
+
"speedPriority" => 0.5
|
21
|
+
},
|
22
|
+
"systemPrompt" => system_prompt,
|
23
|
+
"maxTokens" => max_tokens
|
24
|
+
}
|
25
|
+
|
26
|
+
# Validate before sending
|
27
|
+
errors = ValidationHelpers.validate_create_message_params(params)
|
28
|
+
if errors.any?
|
29
|
+
raise ArgumentError, "Invalid create_message parameters: #{errors.join(", ")}"
|
30
|
+
end
|
31
|
+
|
32
|
+
result = create_message(params)
|
33
|
+
|
34
|
+
# Automatically extract the text content with error handling
|
35
|
+
# Response format: { "role": "assistant", "content": { "type": "text", "text": "..." }, "model": "...", "stopReason": "..." }
|
36
|
+
if result.is_a?(Hash) && result.dig("content", "text")
|
37
|
+
result["content"]["text"]
|
38
|
+
else
|
39
|
+
raise "Unexpected response format from create_message: #{result.inspect}"
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# More advanced helper that handles different message types
|
44
|
+
def chat_with_assistant(messages, system_prompt: "You are a helpful assistant.", max_tokens: 100, model_hints: ["o4-mini"])
|
45
|
+
# Normalize messages to the expected format
|
46
|
+
normalized_messages = messages.map do |msg|
|
47
|
+
case msg
|
48
|
+
when String
|
49
|
+
{"role" => "user", "content" => {"type" => "text", "text" => msg}}
|
50
|
+
when Hash
|
51
|
+
# Ensure string keys and validate structure
|
52
|
+
normalized = ValidationHelpers.stringify_keys(msg)
|
53
|
+
unless normalized.key?("role") && normalized.key?("content")
|
54
|
+
raise ArgumentError, "Message hash must have 'role' and 'content' keys: #{msg.inspect}"
|
55
|
+
end
|
56
|
+
normalized
|
57
|
+
else
|
58
|
+
raise ArgumentError, "Messages must be strings or hashes, got: #{msg.class}"
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
params = {
|
63
|
+
"messages" => normalized_messages,
|
64
|
+
"modelPreferences" => {
|
65
|
+
"hints" => model_hints.map { |name| {"name" => name} },
|
66
|
+
"intelligencePriority" => 0.8,
|
67
|
+
"speedPriority" => 0.5
|
68
|
+
},
|
69
|
+
"systemPrompt" => system_prompt,
|
70
|
+
"maxTokens" => max_tokens
|
71
|
+
}
|
72
|
+
|
73
|
+
# Validate before sending
|
74
|
+
errors = ValidationHelpers.validate_create_message_params(params)
|
75
|
+
if errors.any?
|
76
|
+
raise ArgumentError, "Invalid create_message parameters: #{errors.join(", ")}"
|
77
|
+
end
|
78
|
+
|
79
|
+
result = create_message(params)
|
80
|
+
|
81
|
+
# Automatically extract the text content with error handling
|
82
|
+
# Response format: { "role": "assistant", "content": { "type": "text", "text": "..." }, "model": "...", "stopReason": "..." }
|
83
|
+
if result.is_a?(Hash) && result.dig("content", "text")
|
84
|
+
result["content"]["text"]
|
85
|
+
else
|
86
|
+
raise "Unexpected response format from create_message: #{result.inspect}"
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
# Safe wrapper around create_message with validation
|
91
|
+
def safe_create_message(params)
|
92
|
+
# Ensure all keys are strings
|
93
|
+
safe_params = ValidationHelpers.stringify_keys(params)
|
94
|
+
|
95
|
+
# Validate
|
96
|
+
errors = ValidationHelpers.validate_create_message_params(safe_params)
|
97
|
+
if errors.any?
|
98
|
+
raise ArgumentError, "Invalid create_message parameters: #{errors.join(", ")}"
|
99
|
+
end
|
100
|
+
|
101
|
+
create_message(safe_params)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
@@ -0,0 +1,125 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module MicroMcp
|
4
|
+
module Schema
|
5
|
+
# Schema builder class for chaining
|
6
|
+
class SchemaBuilder
|
7
|
+
def initialize(schema)
|
8
|
+
@schema = schema
|
9
|
+
end
|
10
|
+
|
11
|
+
def required
|
12
|
+
@schema.merge(required: true)
|
13
|
+
end
|
14
|
+
|
15
|
+
def optional
|
16
|
+
@schema
|
17
|
+
end
|
18
|
+
|
19
|
+
def method_missing(method, *args)
|
20
|
+
if @schema.respond_to?(method)
|
21
|
+
@schema.send(method, *args)
|
22
|
+
else
|
23
|
+
super
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def respond_to_missing?(method, include_private = false)
|
28
|
+
@schema.respond_to?(method) || super
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# Alternative builder for required-first syntax
|
33
|
+
class RequiredBuilder
|
34
|
+
def integer(description = nil)
|
35
|
+
schema = {type: "integer"}
|
36
|
+
schema[:description] = description if description
|
37
|
+
schema.merge(required: true)
|
38
|
+
end
|
39
|
+
|
40
|
+
def string(description = nil)
|
41
|
+
schema = {type: "string"}
|
42
|
+
schema[:description] = description if description
|
43
|
+
schema.merge(required: true)
|
44
|
+
end
|
45
|
+
|
46
|
+
def number(description = nil)
|
47
|
+
schema = {type: "number"}
|
48
|
+
schema[:description] = description if description
|
49
|
+
schema.merge(required: true)
|
50
|
+
end
|
51
|
+
|
52
|
+
def boolean(description = nil)
|
53
|
+
schema = {type: "boolean"}
|
54
|
+
schema[:description] = description if description
|
55
|
+
schema.merge(required: true)
|
56
|
+
end
|
57
|
+
|
58
|
+
def array(items_type = nil, description = nil)
|
59
|
+
schema = {type: "array"}
|
60
|
+
schema[:items] = items_type if items_type
|
61
|
+
schema[:description] = description if description
|
62
|
+
schema.merge(required: true)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
# Helper methods for common schema patterns with builder support
|
67
|
+
def self.integer(description = nil)
|
68
|
+
schema = {type: "integer"}
|
69
|
+
schema[:description] = description if description
|
70
|
+
SchemaBuilder.new(schema)
|
71
|
+
end
|
72
|
+
|
73
|
+
def self.string(description = nil)
|
74
|
+
schema = {type: "string"}
|
75
|
+
schema[:description] = description if description
|
76
|
+
SchemaBuilder.new(schema)
|
77
|
+
end
|
78
|
+
|
79
|
+
def self.number(description = nil)
|
80
|
+
schema = {type: "number"}
|
81
|
+
schema[:description] = description if description
|
82
|
+
SchemaBuilder.new(schema)
|
83
|
+
end
|
84
|
+
|
85
|
+
def self.boolean(description = nil)
|
86
|
+
schema = {type: "boolean"}
|
87
|
+
schema[:description] = description if description
|
88
|
+
SchemaBuilder.new(schema)
|
89
|
+
end
|
90
|
+
|
91
|
+
def self.array(items_type = nil, description = nil)
|
92
|
+
schema = {type: "array"}
|
93
|
+
schema[:items] = items_type if items_type
|
94
|
+
schema[:description] = description if description
|
95
|
+
SchemaBuilder.new(schema)
|
96
|
+
end
|
97
|
+
|
98
|
+
# Create object schema with properties and required fields
|
99
|
+
def self.object(**properties)
|
100
|
+
required_fields = []
|
101
|
+
schema_properties = {}
|
102
|
+
|
103
|
+
properties.each do |key, value|
|
104
|
+
if value.is_a?(Hash) && value[:required] == true
|
105
|
+
required_fields << key.to_s
|
106
|
+
value = value.dup
|
107
|
+
value.delete(:required)
|
108
|
+
end
|
109
|
+
schema_properties[key] = value
|
110
|
+
end
|
111
|
+
|
112
|
+
schema = {
|
113
|
+
type: "object",
|
114
|
+
properties: schema_properties
|
115
|
+
}
|
116
|
+
schema[:required] = required_fields unless required_fields.empty?
|
117
|
+
schema
|
118
|
+
end
|
119
|
+
|
120
|
+
# Entry point for required-first syntax
|
121
|
+
def self.required
|
122
|
+
RequiredBuilder.new
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
data/lib/micro_mcp/server.rb
CHANGED
@@ -1,11 +1,74 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require_relative "runtime_helpers"
|
4
|
+
|
3
5
|
module MicroMcp
|
4
6
|
module ToolRegistry
|
5
|
-
def self.register_tool(name:, description: nil, &block)
|
7
|
+
def self.register_tool(name:, description: nil, arguments: nil, &block)
|
8
|
+
raise ArgumentError, "block required" unless block
|
9
|
+
|
10
|
+
# Wrap the block with error handling for all tools
|
11
|
+
wrapped_block = proc do |args, runtime|
|
12
|
+
block.call(args, runtime)
|
13
|
+
rescue => e
|
14
|
+
# For test tools that are designed to fail, re-raise the error
|
15
|
+
# so tests can verify the error behavior
|
16
|
+
if name.to_s.include?("error") ||
|
17
|
+
name.to_s.include?("fail") ||
|
18
|
+
name.to_s.include?("use_captured_runtime") ||
|
19
|
+
e.message.include?("McpServer reference")
|
20
|
+
raise e
|
21
|
+
end
|
22
|
+
|
23
|
+
# Better error reporting for unexpected failures
|
24
|
+
error_msg = "Tool '#{name}' failed: #{e.message}"
|
25
|
+
puts "ERROR: #{error_msg}"
|
26
|
+
puts "Backtrace: #{e.backtrace.first(3).join("\n")}" if ENV["MCP_DEBUG"]
|
27
|
+
error_msg
|
28
|
+
end
|
29
|
+
|
30
|
+
MicroMcpNative.register_tool(name, description, arguments, wrapped_block)
|
31
|
+
end
|
32
|
+
|
33
|
+
# Enhanced registration with better error handling and validation
|
34
|
+
def self.register_assistant_tool(name:, description:, question_param: "question", &block)
|
6
35
|
raise ArgumentError, "block required" unless block
|
7
36
|
|
8
|
-
|
37
|
+
arguments = Schema.object(
|
38
|
+
question_param.to_sym => Schema.string("Question for the assistant").required
|
39
|
+
)
|
40
|
+
|
41
|
+
register_tool(name: name, description: description, arguments: arguments) do |args, runtime|
|
42
|
+
# Extend runtime with helper methods
|
43
|
+
runtime.extend(RuntimeHelpers)
|
44
|
+
|
45
|
+
result = block.call(args, runtime)
|
46
|
+
|
47
|
+
# Auto-handle common return value patterns
|
48
|
+
case result
|
49
|
+
when Hash
|
50
|
+
# If it looks like a create_message result, extract the text
|
51
|
+
# Response format: { "role": "assistant", "content": { "type": "text", "text": "..." }, ... }
|
52
|
+
if result.dig("content", "text")
|
53
|
+
result["content"]["text"]
|
54
|
+
else
|
55
|
+
result
|
56
|
+
end
|
57
|
+
else
|
58
|
+
result
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
# Specialized method for simple question-answering tools
|
64
|
+
def self.register_qa_tool(name:, description:, system_prompt: "You are a helpful assistant.", max_tokens: 100)
|
65
|
+
register_assistant_tool(name: name, description: description) do |args, runtime|
|
66
|
+
runtime.ask_assistant(
|
67
|
+
args["question"],
|
68
|
+
system_prompt: system_prompt,
|
69
|
+
max_tokens: max_tokens
|
70
|
+
)
|
71
|
+
end
|
9
72
|
end
|
10
73
|
end
|
11
74
|
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module MicroMcp
|
4
|
+
module ValidationHelpers
|
5
|
+
# Validate create_message parameters before sending
|
6
|
+
def self.validate_create_message_params(params)
|
7
|
+
errors = []
|
8
|
+
|
9
|
+
# Check required fields
|
10
|
+
errors << "Missing 'messages' field" unless params.key?("messages")
|
11
|
+
errors << "Missing 'maxTokens' field" unless params.key?("maxTokens")
|
12
|
+
|
13
|
+
if params["messages"]
|
14
|
+
# Validate messages structure
|
15
|
+
if params["messages"].is_a?(Array)
|
16
|
+
params["messages"].each_with_index do |msg, i|
|
17
|
+
unless msg.is_a?(Hash)
|
18
|
+
errors << "Message #{i} must be a hash"
|
19
|
+
next
|
20
|
+
end
|
21
|
+
|
22
|
+
unless msg.key?("role")
|
23
|
+
errors << "Message #{i} missing 'role' field"
|
24
|
+
end
|
25
|
+
|
26
|
+
unless msg.key?("content")
|
27
|
+
errors << "Message #{i} missing 'content' field"
|
28
|
+
end
|
29
|
+
|
30
|
+
if msg["content"] && !msg["content"].is_a?(Hash)
|
31
|
+
errors << "Message #{i} 'content' must be a hash"
|
32
|
+
end
|
33
|
+
end
|
34
|
+
else
|
35
|
+
errors << "'messages' must be an array"
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# Check for common mistakes
|
40
|
+
if params.any? { |k, v| k.is_a?(Symbol) }
|
41
|
+
errors << "Hash keys must be strings, not symbols. Use string keys throughout."
|
42
|
+
end
|
43
|
+
|
44
|
+
errors
|
45
|
+
end
|
46
|
+
|
47
|
+
# Helper to convert symbol keys to string keys recursively
|
48
|
+
def self.stringify_keys(obj)
|
49
|
+
case obj
|
50
|
+
when Hash
|
51
|
+
obj.transform_keys(&:to_s).transform_values { |v| stringify_keys(v) }
|
52
|
+
when Array
|
53
|
+
obj.map { |v| stringify_keys(v) }
|
54
|
+
else
|
55
|
+
obj
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
data/lib/micro_mcp/version.rb
CHANGED
data/lib/micro_mcp.rb
CHANGED
@@ -1,9 +1,21 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require_relative "micro_mcp/version"
|
4
|
-
|
4
|
+
ruby_version = "#{RUBY_VERSION[/\d+\.\d+/]}"
|
5
|
+
begin
|
6
|
+
require_relative "micro_mcp/micro_mcp"
|
7
|
+
rescue LoadError
|
8
|
+
begin
|
9
|
+
require_relative "micro_mcp/#{ruby_version}/micro_mcp"
|
10
|
+
rescue LoadError
|
11
|
+
raise LoadError, "No native extension found for Ruby #{ruby_version}"
|
12
|
+
end
|
13
|
+
end
|
14
|
+
require_relative "micro_mcp/schema"
|
5
15
|
require_relative "micro_mcp/tool_registry"
|
6
16
|
require_relative "micro_mcp/server"
|
17
|
+
require_relative "micro_mcp/runtime_helpers"
|
18
|
+
require_relative "micro_mcp/validation_helpers"
|
7
19
|
|
8
20
|
module MicroMcp
|
9
21
|
class Error < StandardError; end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: micro_mcp
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.4
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Erwin Kroon
|
@@ -15,14 +15,14 @@ dependencies:
|
|
15
15
|
requirements:
|
16
16
|
- - "~>"
|
17
17
|
- !ruby/object:Gem::Version
|
18
|
-
version: 0.9.
|
18
|
+
version: 0.9.116
|
19
19
|
type: :runtime
|
20
20
|
prerelease: false
|
21
21
|
version_requirements: !ruby/object:Gem::Requirement
|
22
22
|
requirements:
|
23
23
|
- - "~>"
|
24
24
|
- !ruby/object:Gem::Version
|
25
|
-
version: 0.9.
|
25
|
+
version: 0.9.116
|
26
26
|
description: ''
|
27
27
|
email:
|
28
28
|
- 123574+ekroon@users.noreply.github.com
|
@@ -33,6 +33,7 @@ extra_rdoc_files: []
|
|
33
33
|
files:
|
34
34
|
- ".devcontainer/devcontainer.json"
|
35
35
|
- ".standard.yml"
|
36
|
+
- ".tool-versions"
|
36
37
|
- ".vscode/mcp.json"
|
37
38
|
- AGENTS.md
|
38
39
|
- CHANGELOG.md
|
@@ -41,16 +42,20 @@ files:
|
|
41
42
|
- LICENSE.txt
|
42
43
|
- README.md
|
43
44
|
- Rakefile
|
45
|
+
- docs/changes/ERGONOMIC_IMPROVEMENTS.md
|
46
|
+
- ext/micro_mcp/AGENTS.md
|
44
47
|
- ext/micro_mcp/Cargo.toml
|
45
48
|
- ext/micro_mcp/extconf.rb
|
46
49
|
- ext/micro_mcp/src/lib.rs
|
47
50
|
- ext/micro_mcp/src/server.rs
|
48
51
|
- ext/micro_mcp/src/utils.rs
|
49
52
|
- lib/micro_mcp.rb
|
53
|
+
- lib/micro_mcp/runtime_helpers.rb
|
54
|
+
- lib/micro_mcp/schema.rb
|
50
55
|
- lib/micro_mcp/server.rb
|
51
56
|
- lib/micro_mcp/tool_registry.rb
|
57
|
+
- lib/micro_mcp/validation_helpers.rb
|
52
58
|
- lib/micro_mcp/version.rb
|
53
|
-
- sig/micro_mcp.rbs
|
54
59
|
homepage: https://github.com/ekroon/micro_mcp
|
55
60
|
licenses:
|
56
61
|
- MIT
|
data/sig/micro_mcp.rbs
DELETED