Skip to content

Commit f1835b3

Browse files
committed
refactor: Implement prompt functionality feedback
Key changes: - Added robust prompt registration and handling in Server class - Implemented base64 validation for image content to ensure MCP compliance - Simplified messages API to use only keyword arguments for better usability - Refactored code to follow DRY principles and improve maintainability - Updated tool_name method to follow the same pattern as prompt_name - Fixed unused parameters and linter warnings throughout the codebase - Added comprehensive test coverage for all new functionality - Added example prompts to demo files for easy adoption - Reverted version to 1.0.0 as requested by maintainer This implementation enables developers to create and register prompts that can generate messages with text, image, and resource content, following the MCP specification. The code is thoroughly tested and includes examples to demonstrate proper usage. All tasks from PR yjacquin#21 feedback have been addressed
1 parent 228522c commit f1835b3

File tree

13 files changed

+270
-129
lines changed

13 files changed

+270
-129
lines changed

CHANGELOG.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8-
## [1.2.0] - 2025-04-14
8+
## [Unreleased]
99

1010
### Added
1111
- Prompts Feature
@@ -40,7 +40,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
4040
- Namespace consistency correction (FastMCP -> FastMcp) throughout the codebase
4141

4242
### Improved
43-
- ⚠️ [Breaking] Resource content declaration changes
43+
- [Breaking] Resource content declaration changes
4444
- Now resources implement `content` over `default_content`
4545
- `content` is dynamically called when calling a resource, this implies we can declare dynamic resource contents like:
4646
```ruby

Gemfile.lock

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
PATH
22
remote: .
33
specs:
4-
fast-mcp (1.1.0)
4+
fast-mcp (1.0.0)
55
base64
66
dry-schema (~> 1.14)
77
json (~> 2.0)

README.md

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,29 @@ end
219219
# Register the resource with the server
220220
server.register_resource(StatisticsResource)
221221

222+
# Define a prompt by inheriting from FastMcp::Prompt
223+
class GreetingPrompt < FastMcp::Prompt
224+
prompt_name 'greeting'
225+
description 'A friendly greeting prompt'
226+
227+
arguments do
228+
required(:name).filled(:string).description("User's name")
229+
optional(:time_of_day).filled(:string).description("Morning, afternoon, or evening")
230+
end
231+
232+
def call(name:, time_of_day: nil)
233+
greeting = time_of_day ? "Good #{time_of_day}" : "Hello"
234+
235+
messages(
236+
assistant: "#{greeting}, #{name}! How can I help you today?",
237+
user: "I'd like some assistance with Ruby programming."
238+
)
239+
end
240+
end
241+
242+
# Register the prompt with the server
243+
server.register_prompt(GreetingPrompt)
244+
222245
# Start the server
223246
server.start
224247
```
@@ -388,7 +411,4 @@ This project is available as open source under the terms of the [MIT License](ht
388411

389412
- The [Model Context Protocol](https://github.com/modelcontextprotocol) team for creating the specification
390413
- The [Dry-Schema](https://github.com/dry-rb/dry-schema) team for the argument validation.
391-
- All contributors to this project
392-
393-
## How to add a MCP server to Claude, Cursor, or other MCP clients?
394-
Please refer to [configuring_mcp_clients](docs/configuring_mcp_clients.md)
414+
- All contributors to this project

examples/authenticated_rack_middleware.rb

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,35 @@ def content
5959
end
6060
end
6161

62+
# Example prompt that uses inline text instead of ERB templates
63+
class InlinePrompt < FastMcp::Prompt
64+
prompt_name 'inline_example'
65+
description 'An example prompt that uses inline text instead of ERB templates'
66+
67+
arguments do
68+
required(:query).description('The user query to respond to')
69+
optional(:context).description('Additional context for the response')
70+
end
71+
72+
def call(query:, context: nil)
73+
# Create assistant message
74+
assistant_message = "I'll help you answer your question about: #{query}"
75+
76+
# Create user message
77+
user_message = if context
78+
"My question is: #{query}\nHere's some additional context: #{context}"
79+
else
80+
"My question is: #{query}"
81+
end
82+
83+
# Using the messages method with a hash
84+
messages(
85+
assistant: assistant_message,
86+
user: user_message
87+
)
88+
end
89+
end
90+
6291
# Create a simple Rack application
6392
app = lambda do |_env|
6493
[200, { 'Content-Type' => 'text/html' },
@@ -73,6 +102,9 @@ def content
73102

74103
# Register a sample resource
75104
server.register_resource(HelloWorldResource)
105+
106+
# Register the inline prompt
107+
server.register_prompt(InlinePrompt)
76108
end
77109

78110
# Run the Rack application with Puma

examples/prompts/multi_message_prompt.rb

Lines changed: 0 additions & 46 deletions
This file was deleted.

examples/rack_middleware.rb

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,35 @@ def content
5959
end
6060
end
6161

62+
# Example prompt that uses inline text instead of ERB templates
63+
class InlinePrompt < FastMcp::Prompt
64+
prompt_name 'inline_example'
65+
description 'An example prompt that uses inline text instead of ERB templates'
66+
67+
arguments do
68+
required(:query).description('The user query to respond to')
69+
optional(:context).description('Additional context for the response')
70+
end
71+
72+
def call(query:, context: nil)
73+
# Create assistant message
74+
assistant_message = "I'll help you answer your question about: #{query}"
75+
76+
# Create user message
77+
user_message = if context
78+
"My question is: #{query}\nHere's some additional context: #{context}"
79+
else
80+
"My question is: #{query}"
81+
end
82+
83+
# Using the messages method with a hash
84+
messages(
85+
assistant: assistant_message,
86+
user: user_message
87+
)
88+
end
89+
end
90+
6291
# Create a simple Rack application
6392
app = lambda do |_env|
6493
[200, { 'Content-Type' => 'text/html' },
@@ -76,6 +105,9 @@ def content
76105

77106
# Register a sample resource
78107
server.register_resource(HelloWorldResource)
108+
109+
# Register a sample prompt
110+
server.register_prompt(InlinePrompt)
79111
end
80112

81113
# Run the Rack application with Puma

examples/server_with_stdio_transport.rb

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,37 @@ def content
7777
end
7878
end
7979

80+
# Example prompt that uses inline text instead of ERB templates
81+
class InlinePrompt < FastMcp::Prompt
82+
prompt_name 'inline_example'
83+
description 'An example prompt that uses inline text instead of ERB templates'
84+
85+
arguments do
86+
required(:query).description('The user query to respond to')
87+
optional(:context).description('Additional context for the response')
88+
end
89+
90+
def call(query:, context: nil)
91+
# Create assistant message
92+
assistant_message = "I'll help you answer your question about: #{query}"
93+
94+
# Create user message
95+
user_message = if context
96+
"My question is: #{query}\nHere's some additional context: #{context}"
97+
else
98+
"My question is: #{query}"
99+
end
100+
101+
# Using the messages method with a hash
102+
messages(
103+
assistant: assistant_message,
104+
user: user_message
105+
)
106+
end
107+
end
108+
80109
server.register_resources(CounterResource, UsersResource, WeatherResource)
110+
server.register_prompts(InlinePrompt)
81111

82112
# Class-based tool for incrementing the counter
83113
class IncrementCounterTool < FastMcp::Tool

lib/mcp/prompt.rb

Lines changed: 30 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
require 'dry-schema'
44
require 'erb'
5+
require 'base64'
56

67
module FastMcp
78
# Main Prompt class that represents an MCP Prompt
@@ -77,72 +78,30 @@ def message(role:, content:)
7778
}
7879
end
7980

80-
# Create multiple messages from a hash of role => content pairs or an array of role-keyed hashes
81-
# @param args [Hash, Array<Hash>] Either a hash of role => content pairs or an array of single-key hashes with role as key
81+
# Create multiple messages from a hash of role => content pairs
82+
# @param messages_hash [Hash] A hash of role => content pairs
8283
# @return [Array<Hash>] An array of messages
83-
def messages(*args)
84-
result = []
84+
def messages(messages_hash)
85+
raise ArgumentError, 'At least one message must be provided' if messages_hash.empty?
8586

86-
# Handle both hash and array arguments for backward compatibility
87-
if args.length == 1 && args.first.is_a?(Hash)
88-
# Original API: hash of role => content pairs
89-
messages_hash = args.first
90-
raise ArgumentError, 'At least one message must be provided' if messages_hash.empty?
91-
92-
messages_hash.each do |role_key, content|
93-
# Extract the base role (without any suffix like _2)
94-
base_role = role_key.to_s.gsub(/_\d+$/, '')
95-
96-
# Validate the role
97-
validate_role(base_role)
98-
99-
# Add the message to the result
100-
if content.is_a?(Hash)
101-
result << message(role: base_role, content: content)
102-
else
103-
result << message(role: base_role, content: text_content(content))
104-
end
105-
end
106-
else
107-
# New API: array of single-key hashes with role as key
108-
raise ArgumentError, 'At least one message must be provided' if args.empty?
109-
110-
args.each do |msg_hash|
111-
raise ArgumentError, 'Each message must be a hash' unless msg_hash.is_a?(Hash)
112-
113-
# Each hash should have a single key (the role)
114-
msg_hash.each do |role_key, content|
115-
# Extract the base role (without any suffix like _2)
116-
base_role = role_key.to_s.gsub(/_\d+$/, '')
117-
118-
# Validate the role
119-
validate_role(base_role)
120-
121-
# Add the message to the result
122-
if content.is_a?(Hash)
123-
result << message(role: base_role, content: content)
124-
else
125-
result << message(role: base_role, content: text_content(content))
126-
end
127-
end
128-
end
87+
messages_hash.map do |role_key, content|
88+
role = role_key.to_s.gsub(/_\d+$/, '').to_sym
89+
{ role: ROLES.fetch(role), content: content_from(content) }
12990
end
130-
131-
result
13291
end
13392

13493
# Helper method to extract content from a hash
135-
def content_from_hash(hash)
136-
if hash.key?(:text)
137-
text_content(hash[:text])
138-
elsif hash.key?(:data) && hash.key?(:mimeType)
139-
image_content(hash[:data], hash[:mimeType])
140-
elsif hash.key?(:resource)
141-
# It's already a resource content
94+
def content_from(content)
95+
if content.is_a?(String)
96+
text_content(content)
97+
elsif content.key?(:text)
98+
text_content(content[:text])
99+
elsif content.key?(:data) && content.key?(:mimeType)
100+
image_content(content[:data], content[:mimeType])
101+
elsif content.key?(:resource)
142102
hash
143103
else
144-
# Default to text content with empty string
145-
text_content('')
104+
text_content('unsupported content')
146105
end
147106
end
148107

@@ -200,6 +159,19 @@ def validate_content(content)
200159
when CONTENT_TYPE_IMAGE
201160
raise ArgumentError, "Missing :data in image content" unless content[:data]
202161
raise ArgumentError, "Missing :mimeType in image content" unless content[:mimeType]
162+
163+
# Validate that data is a string
164+
unless content[:data].is_a?(String)
165+
raise ArgumentError, "Image :data must be a string containing base64-encoded data"
166+
end
167+
168+
# Validate that data is valid base64
169+
begin
170+
# Try to decode the base64 data
171+
Base64.strict_decode64(content[:data])
172+
rescue ArgumentError
173+
raise ArgumentError, "Image :data must be valid base64-encoded data"
174+
end
203175
when CONTENT_TYPE_RESOURCE
204176
validate_resource_content(content[:resource])
205177
else

0 commit comments

Comments
 (0)