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.
@@ -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
@@ -13,11 +13,11 @@ module MicroMcp
13
13
  thread.join
14
14
  rescue Interrupt
15
15
  puts "\nShutting down server..."
16
- thread.kill
16
+ MicroMcpNative.shutdown_server
17
+ thread.join
17
18
  end
18
19
 
19
20
  puts "Server stopped."
20
- Process.kill("KILL", Process.pid)
21
21
  end
22
22
  end
23
23
  end
@@ -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
- MicroMcpNative.register_tool(name, description, block)
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module MicroMcp
4
- VERSION = "0.1.0"
4
+ VERSION = "0.1.4"
5
5
  end
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
- require_relative "micro_mcp/micro_mcp"
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.0
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.91
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.91
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
@@ -1,4 +0,0 @@
1
- module MicroMcp
2
- VERSION: String
3
- # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
- end