Internal Release v0.1.1

- implement `Prompt`
- adjust Tool approach
- update readme for tool calling
- document usage of `Prompt`
This commit is contained in:
Topher Bullock 2025-02-27 10:33:57 -05:00
commit a4df3b8688
34 changed files with 1530 additions and 0 deletions

4
.gitattributes vendored Normal file
View file

@ -0,0 +1,4 @@
# See https://git-scm.com/docs/gitattributes for more about git attribute files.
# Mark any vendored files as having been vendored.
vendor/* linguist-vendored

22
.github/workflows/ci.yml vendored Normal file
View file

@ -0,0 +1,22 @@
name: CI
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
entry:
- { ruby: 3.2, allowed-failure: false }
- { ruby: 3.3, allowed-failure: false }
- { ruby: 3.4, allowed-failure: false }
- { ruby: head, allowed-failure: true }
name: Test Ruby ${{ matrix.entry.ruby }}
steps:
- uses: actions/checkout@v3
- uses: ruby/setup-ruby@v1
with:
ruby-version: ${{ matrix.entry.ruby }}
bundler-cache: true
- run: bundle exec rake test
continue-on-error: ${{ matrix.entry.allowed-failure }}

8
.gitignore vendored Normal file
View file

@ -0,0 +1,8 @@
/.bundle/
/.yardoc
/_yardoc/
/coverage/
/doc/
/pkg/
/spec/reports/
/tmp/

2
.rubocop.yml Normal file
View file

@ -0,0 +1,2 @@
inherit_gem:
rubocop-shopify: rubocop.yml

1
.ruby-version Normal file
View file

@ -0,0 +1 @@
3.3.0

74
CODE_OF_CONDUCT.md Normal file
View file

@ -0,0 +1,74 @@
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, gender identity and expression, level of experience,
nationality, personal appearance, race, religion, or sexual identity and
orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment
include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or
advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic
address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at topher.bullock@shopify.com. All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
available at [http://contributor-covenant.org/version/1/4][version]
[homepage]: http://contributor-covenant.org
[version]: http://contributor-covenant.org/version/1/4/

15
Gemfile Normal file
View file

@ -0,0 +1,15 @@
# frozen_string_literal: true
source "https://rubygems.org"
# Specify runtime dependencies in the gemspec
gemspec
# Specify development dependencies below
gem "minitest", "~> 5.1", require: false
gem "rake", "~> 13.0"
gem "rubocop-shopify", require: false
gem "activesupport"
gem "minitest-reporters"
gem "mocha"

95
Gemfile.lock Normal file
View file

@ -0,0 +1,95 @@
PATH
remote: .
specs:
model_context_protocol (0.1.1)
activesupport
GEM
remote: https://rubygems.org/
specs:
activesupport (8.0.2)
base64
benchmark (>= 0.3)
bigdecimal
concurrent-ruby (~> 1.0, >= 1.3.1)
connection_pool (>= 2.2.5)
drb
i18n (>= 1.6, < 2)
logger (>= 1.4.2)
minitest (>= 5.1)
securerandom (>= 0.3)
tzinfo (~> 2.0, >= 2.0.5)
uri (>= 0.13.1)
ansi (1.5.0)
ast (2.4.3)
base64 (0.2.0)
benchmark (0.4.0)
bigdecimal (3.1.9)
builder (3.3.0)
concurrent-ruby (1.3.5)
connection_pool (2.5.3)
drb (2.2.1)
i18n (1.14.7)
concurrent-ruby (~> 1.0)
json (2.11.3)
language_server-protocol (3.17.0.4)
lint_roller (1.1.0)
logger (1.7.0)
minitest (5.25.5)
minitest-reporters (1.7.1)
ansi
builder
minitest (>= 5.0)
ruby-progressbar
mocha (2.7.1)
ruby2_keywords (>= 0.0.5)
parallel (1.27.0)
parser (3.3.8.0)
ast (~> 2.4.1)
racc
prism (1.4.0)
racc (1.8.1)
rainbow (3.1.1)
rake (13.2.1)
regexp_parser (2.10.0)
rubocop (1.75.5)
json (~> 2.3)
language_server-protocol (~> 3.17.0.2)
lint_roller (~> 1.1.0)
parallel (~> 1.10)
parser (>= 3.3.0.2)
rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 2.9.3, < 3.0)
rubocop-ast (>= 1.44.0, < 2.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 2.4.0, < 4.0)
rubocop-ast (1.44.1)
parser (>= 3.3.7.2)
prism (~> 1.4)
rubocop-shopify (2.17.0)
rubocop (~> 1.62)
ruby-progressbar (1.13.0)
ruby2_keywords (0.0.5)
securerandom (0.4.1)
tzinfo (2.0.6)
concurrent-ruby (~> 1.0)
unicode-display_width (3.1.4)
unicode-emoji (~> 4.0, >= 4.0.4)
unicode-emoji (4.0.4)
uri (1.0.3)
PLATFORMS
arm64-darwin-23
ruby
DEPENDENCIES
activesupport
minitest (~> 5.1)
minitest-reporters
mocha
model_context_protocol!
rake (~> 13.0)
rubocop-shopify
BUNDLED WITH
2.5.9

21
LICENSE.txt Normal file
View file

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2025 TODO: Write your name
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

102
README.md Normal file
View file

@ -0,0 +1,102 @@
# Model Context Protocol
A Ruby gem implementing Model Context Protocol + JSON RPC 2.0 spec
## Overview
Model Context Protocol provides a standardized way to interact with language models through a JSON RPC 2.0 interface.
It handles request/response formatting, error handling, and tool integrations in a type-safe manner using Sorbet.
See https://spec.modelcontextprotocol.io/specification/
This implementation breaks from the transport examples in the spec as it implements a POST-based JSON RPC call rather
than a stateful SSE transport.
## MCP Server
The `ModelContextProtocol::Server` class is the core component that handles JSON-RPC requests and responses.
It implements the Model Context Protocol specification, handling model context requests and responses.
### Key Features
- Implements JSON-RPC 2.0 message handling
- Supports protocol initialization and capability negotiation
- Manages tool registration and invocation
- Provides type-safe request/response handling using Sorbet
### Supported Methods
- `initialize` - Initializes the protocol and returns server capabilities
- `ping` - Simple health check that returns "pong"
- `tools/list` - Lists all registered tools and their schemas
- `tools/call` - Invokes a specific tool with provided arguments
- `prompts/list` - Lists all registered prompts and their schemas
- `prompts/get` - Retrieves a specific prompt by name
### Usage
Implement an `ApplicationController` which calls the `Server#handle` method, eg
```ruby
module ModelContextProtocol
class ApplicationController < ActionController::Base
sig { void }
def index
server = ModelContextProtocol::Server.new(
name: "my_server",
tools: [someTool, anotherTool],
prompts: [myPrompt]
)
render(json: server.handle(request.body.read).to_h)
end
end
end
```
## Tools
MCP spec includes [Tools](https://modelcontextprotocol.io/docs/concepts/tools) which provide functionality to LLM apps.
This gem provides a `ModelContextProtocol::Tool` class that can be instantiated to create tools.
Tools can be passed into the `ModelContextProtocol::Server` constructor to register them with the server.
### Example Tool Implementation
The `Tool` class allows creating tools that can be used within the model context.
To create a tool, instantiate the class with the required parameters and an optional block:
```ruby
tool = ModelContextProtocol::Tool.new(name: "my_tool", description: "This tool performs specific functionality...") do |args|
# Implement the tool's functionality here
result = process_something(args["parameter_name"])
ModelContextProtocol::Tool::Response.new([{ type: "text", text: result }], false )
end
```
## Prompts
MCP spec includes
[Prompts](https://modelcontextprotocol.io/docs/concepts/prompts), `Prompts` enable servers to define reusable prompt
templates and workflows that clients can easily surface to users and LLMs
The `Prompt` class allows creating prompts that can be used within the model context.
To create a prompt, instantiate the class with the required parameters and an optional block:
```ruby
prompt = ModelContextProtocol::Prompt.new(name: "my_prompt", description: "This prompt performs specific functionality...") do |args|
# Implement the prompt's functionality here
result = template_something(args["parameter_name"])
ModelContextProtocol::Prompt::Response.new([{ type: "text", text: result }], false )
end
```
## Releases
TODO

17
Rakefile Normal file
View file

@ -0,0 +1,17 @@
# frozen_string_literal: true
require "bundler/gem_tasks"
require "rake/testtask"
Rake::TestTask.new(:test) do |t|
t.ruby_opts = ["-W0", "-W:deprecated"]
t.libs << "test"
t.libs << "lib"
t.test_files = FileList["test/**/*_test.rb"]
end
require "rubocop/rake_task"
RuboCop::RakeTask.new
task default: [:test, :rubocop]

15
bin/console Executable file
View file

@ -0,0 +1,15 @@
#!/usr/bin/env ruby
# frozen_string_literal: true
require "bundler/setup"
require "model_context_protocol"
# You can add fixtures and/or initialization code here to make experimenting
# with your gem easier. You can also use a different console, if you like.
# (If you use this, don't forget to add pry to your Gemfile!)
# require "pry"
# Pry.start
require "irb"
IRB.start(__FILE__)

31
bin/rake Executable file
View file

@ -0,0 +1,31 @@
#!/usr/bin/env ruby
# frozen_string_literal: true
#
# This file was generated by Bundler.
#
# The application 'rake' is installed as part of a gem, and
# this file is here to facilitate running it.
#
require "pathname"
ENV["BUNDLE_GEMFILE"] ||= File.expand_path(
"../../Gemfile",
Pathname.new(__FILE__).realpath,
)
bundle_binstub = File.expand_path("../bundle", __FILE__)
if File.file?(bundle_binstub)
if File.read(bundle_binstub, 300) =~ /This file was generated by Bundler/
load(bundle_binstub)
else
abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
end
end
require "rubygems"
require "bundler/setup"
load Gem.bin_path("rake", "rake")

8
bin/setup Executable file
View file

@ -0,0 +1,8 @@
#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'
set -vx
bundle install
# Do any other automated setup that you need to do here

31
dev.yml Normal file
View file

@ -0,0 +1,31 @@
name: mcp-ruby
type: ruby
up:
- ruby
- bundler
commands:
console:
desc: Open console with the gem loaded
run: bin/console
build:
desc: Build the gem using rake build
run: bin/rake build
test:
desc: Run tests
syntax:
argument: file
optional: args...
run: |
if [[ $# -eq 0 ]]; then
bin/rake test
else
bin/rake -I test "$@"
fi
style:
desc: Run rubocop
aliases: [rubocop, lint]
run: bin/rubocop

View file

@ -0,0 +1,20 @@
# typed: strict
# frozen_string_literal: true
require "model_context_protocol/json_rpc"
require "model_context_protocol/server"
require "model_context_protocol/tool"
require "model_context_protocol/content"
require "model_context_protocol/resource"
require "model_context_protocol/prompt"
module ModelContextProtocol
class Annotations
attr_reader :audience, :priority
def initialize(audience: nil, priority: nil)
@audience = audience
@priority = priority
end
end
end

View file

@ -0,0 +1,33 @@
# typed: true
# frozen_string_literal: true
module ModelContextProtocol
module Content
class Text
attr_reader :text, :annotations
def initialize(text, annotations: nil)
@text = text
@annotations = annotations
end
def to_h
{ text:, annotations: }.compact
end
end
class Image
attr_reader :data, :mime_type, :annotations
def initialize(data, mime_type, annotations: nil)
@data = data
@mime_type = mime_type
@annotations = annotations
end
def to_h
{ data:, mime_type:, annotations: }.compact
end
end
end
end

View file

@ -0,0 +1,11 @@
# frozen_string_literal: true
require "model_context_protocol/json_rpc/error"
require "model_context_protocol/json_rpc/request"
require "model_context_protocol/json_rpc/response"
module ModelContextProtocol
module JsonRPC
VERSION = "2.0"
end
end

View file

@ -0,0 +1,58 @@
# frozen_string_literal: true
module ModelContextProtocol
module JsonRPC
class Error < StandardError
PARSE_ERROR_CODE = -32700
INVALID_REQUEST_CODE = -32600
METHOD_NOT_FOUND_CODE = -32601
INVALID_PARAMS_CODE = -32602
INTERNAL_ERROR_CODE = -32603
attr_reader :code, :message, :data
def initialize(code:, message:, data: nil)
super(message)
@code = code
@message = message
@data = data
end
def to_h
{ code:, message:, data: }.compact
end
end
class InvalidRequestError < Error
def initialize(message:)
super(code: INVALID_REQUEST_CODE, message: message)
end
end
class ParseError < Error
def initialize(message:)
super(code: PARSE_ERROR_CODE, message: message)
end
end
class MethodNotFoundError < Error
def initialize(message:)
super(code: METHOD_NOT_FOUND_CODE, message: message)
end
end
class InvalidParamsError < Error
def initialize(message:)
super(code: INVALID_PARAMS_CODE, message: message)
end
end
class InternalError < Error
def initialize(message:)
super(code: INTERNAL_ERROR_CODE, message: message)
end
end
class InvalidResponse < StandardError; end
end
end

View file

@ -0,0 +1,50 @@
# frozen_string_literal: true
require "json"
module ModelContextProtocol
module JsonRPC
class Request
class << self
def parse(json)
parsed = JSON.parse(json)
from_h(parsed)
rescue JSON::ParserError => e
raise ParseError.new(message: e.message)
end
private
def from_h(hash)
new(version: hash["jsonrpc"], method: hash["method"], params: hash["params"], id: hash["id"])
end
end
attr_reader :version, :method, :params, :id
def initialize(version:, method:, params: nil, id: nil)
@version = version
@method = method
@params = params
@id = id
end
def notification?
id.nil?
end
def valid?
@version == JsonRPC::VERSION && !method.start_with?("rpc.")
end
def method_not_found!
raise MethodNotFoundError.new(message: "Method not found #{method}")
end
def validate!
raise InvalidRequestError.new(message: "Method cannot start with 'rpc.'") if method.start_with?("rpc.")
raise InvalidRequestError.new(message: "Invalid JSON-RPC version") unless valid?
end
end
end
end

Some files were not shown because too many files have changed in this diff Show more