plasma-mcp 0.0.1

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.
Files changed (44) hide show
  1. checksums.yaml +7 -0
  2. data/.rubocop.yml +16 -0
  3. data/.ruby-version +1 -0
  4. data/CHANGELOG.md +26 -0
  5. data/LICENSE +21 -0
  6. data/README.md +207 -0
  7. data/Rakefile +16 -0
  8. data/docs/ROADMAP.md +46 -0
  9. data/exe/plasma +8 -0
  10. data/lib/plasma/application.rb +193 -0
  11. data/lib/plasma/auth.rb +82 -0
  12. data/lib/plasma/auth_server.rb +45 -0
  13. data/lib/plasma/cli.rb +93 -0
  14. data/lib/plasma/component_generator.rb +111 -0
  15. data/lib/plasma/generator.rb +132 -0
  16. data/lib/plasma/loader.rb +32 -0
  17. data/lib/plasma/prompt.rb +10 -0
  18. data/lib/plasma/resource.rb +39 -0
  19. data/lib/plasma/server.rb +47 -0
  20. data/lib/plasma/storage/application_record.rb +10 -0
  21. data/lib/plasma/storage/record.rb +16 -0
  22. data/lib/plasma/storage/variable.rb +53 -0
  23. data/lib/plasma/storage.rb +101 -0
  24. data/lib/plasma/templates/.dockerignore.erb +40 -0
  25. data/lib/plasma/templates/.env.erb +2 -0
  26. data/lib/plasma/templates/.github/workflows/docker-build.yml.erb +52 -0
  27. data/lib/plasma/templates/.gitignore.erb +40 -0
  28. data/lib/plasma/templates/.ruby-version.erb +1 -0
  29. data/lib/plasma/templates/Dockerfile.erb +18 -0
  30. data/lib/plasma/templates/Gemfile.erb +6 -0
  31. data/lib/plasma/templates/README.md.erb +228 -0
  32. data/lib/plasma/templates/app/prompts/example_prompt.rb.erb +30 -0
  33. data/lib/plasma/templates/app/resources/example_resource.rb.erb +36 -0
  34. data/lib/plasma/templates/app/tools/example_tool.rb.erb +35 -0
  35. data/lib/plasma/templates/app/variables/example_variable.erb +20 -0
  36. data/lib/plasma/templates/config/application.rb.erb +24 -0
  37. data/lib/plasma/templates/config/boot.rb.erb +12 -0
  38. data/lib/plasma/templates/config/initializers/example.rb.erb +7 -0
  39. data/lib/plasma/templates/lib/version.rb.erb +5 -0
  40. data/lib/plasma/tool.rb +131 -0
  41. data/lib/plasma/version.rb +5 -0
  42. data/lib/plasma.rb +19 -0
  43. data/sig/plasma.rbs +4 -0
  44. metadata +257 -0
@@ -0,0 +1,52 @@
1
+ name: Build and Publish Multi-Arch Docker Image
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - main
7
+ paths:
8
+ - 'lib/**/version.rb'
9
+ tags:
10
+ - 'v*' # Trigger on all semver-like tags: v1.2.3, v1.2.3-rc.1
11
+
12
+ jobs:
13
+ build-and-push:
14
+ runs-on: ubuntu-latest
15
+ permissions:
16
+ contents: read
17
+ packages: write
18
+
19
+ steps:
20
+ - name: Checkout code
21
+ uses: actions/checkout@v4
22
+
23
+ - name: Extract version from version.rb
24
+ id: version
25
+ run: |
26
+ VERSION=$(ruby -e "require_relative 'lib/<%= name %>/version'; puts <%= name.camelize %>::VERSION")
27
+ echo "VERSION=$VERSION" >> $GITHUB_ENV
28
+ if [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
29
+ echo "IS_STABLE=true" >> $GITHUB_ENV
30
+ else
31
+ echo "IS_STABLE=false" >> $GITHUB_ENV
32
+ fi
33
+
34
+ - name: Set up Docker Buildx
35
+ uses: docker/setup-buildx-action@v3
36
+
37
+ - name: Login to GitHub Container Registry
38
+ uses: docker/login-action@v3
39
+ with:
40
+ registry: ghcr.io
41
+ username: ${{ github.actor }}
42
+ password: ${{ secrets.GITHUB_TOKEN }}
43
+
44
+ - name: Build and push multi-arch Docker image
45
+ uses: docker/build-push-action@v5
46
+ with:
47
+ context: .
48
+ platforms: linux/amd64,linux/arm64
49
+ push: true
50
+ tags: |
51
+ ghcr.io/${{ github.repository }}:v${{ env.VERSION }}
52
+ ${{ env.IS_STABLE == 'true' && format('ghcr.io/{0}:latest', github.repository) || '' }}
@@ -0,0 +1,40 @@
1
+ # Ruby
2
+ *.gem
3
+ *.rbc
4
+ /.config
5
+ /coverage/
6
+ /InstalledFiles
7
+ /pkg/
8
+ /spec/reports/
9
+ /spec/examples.txt
10
+ /test/tmp/
11
+ /test/version_tmp/
12
+ /tmp/
13
+
14
+ # Environment
15
+ .env
16
+ .env.*
17
+ !.env.example
18
+
19
+ # Logs
20
+ *.log
21
+
22
+ # Editor directories and files
23
+ .idea
24
+ .vscode
25
+ *.swp
26
+ *.swo
27
+ *~
28
+
29
+ # OS generated files
30
+ .DS_Store
31
+ .DS_Store?
32
+ ._*
33
+ .Spotlight-V100
34
+ .Trashes
35
+ ehthumbs.db
36
+ Thumbs.db
37
+
38
+ # Docker
39
+ .docker/
40
+ docker-compose.override.yml
@@ -0,0 +1 @@
1
+ 3.4.3
@@ -0,0 +1,18 @@
1
+ FROM ruby:3.4.3-slim
2
+
3
+ WORKDIR /<%= name %>
4
+
5
+ # Install build dependencies
6
+ RUN apt-get update && apt-get install -y \
7
+ build-essential \
8
+ && rm -rf /var/lib/apt/lists/*
9
+
10
+ # Install dependencies
11
+ COPY Gemfile Gemfile.lock ./
12
+ RUN bundle install
13
+
14
+ # Copy application code
15
+ COPY . .
16
+
17
+ # Command to run the application
18
+ CMD ["bundle", "exec", "plasma", "server"]
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "plasma-mcp", "~> <%= Plasma::VERSION %>"
6
+ gem "dotenv", "~> 2.8"
@@ -0,0 +1,228 @@
1
+ # <%= name.camelize %> MCP Server
2
+
3
+ This is a Model Context Protocol (MCP) server built with `plasma-mcp` framework.
4
+
5
+ ## Using with Claude
6
+
7
+ To use this MCP server with Claude, add the following configuration to your Claude settings:
8
+
9
+ ```json
10
+ {
11
+ "mcpServers": {
12
+ "<%= name.underscore %>": {
13
+ "command": "docker",
14
+ "args": [
15
+ "run",
16
+ "-i",
17
+ "--rm",
18
+ "ghcr.io/your-org/<%= name.underscore %>:latest"
19
+ ]
20
+ }
21
+ }
22
+ }
23
+ ```
24
+
25
+ For local development without Docker:
26
+
27
+ ```json
28
+ {
29
+ "mcpServers": {
30
+ "<%= name.underscore %>": {
31
+ "command": "plasma",
32
+ "args": [
33
+ "server",
34
+ "/path/to/<%= name.underscore %>"
35
+ ]
36
+ }
37
+ }
38
+ }
39
+ ```
40
+
41
+ ## Using with VS Code
42
+
43
+ Add the following JSON block to your User Settings (JSON) file in VS Code. You can do this by pressing `Ctrl + Shift + P` and typing `Preferences: Open User Settings (JSON)`.
44
+
45
+ ```json
46
+ {
47
+ "mcp": {
48
+ "servers": {
49
+ "<%= name.underscore %>": {
50
+ "command": "docker",
51
+ "args": [
52
+ "run",
53
+ "-i",
54
+ "--rm",
55
+ "ghcr.io/your-org/<%= name.underscore %>:latest"
56
+ ]
57
+ }
58
+ }
59
+ }
60
+ }
61
+ ```
62
+
63
+ Optionally, you can add a similar example (i.e. without the `mcp` key) to a file called `.vscode/mcp.json` in your workspace. This will allow you to share the configuration with others.
64
+
65
+ ```json
66
+ {
67
+ "servers": {
68
+ "<%= name.underscore %>": {
69
+ "command": "docker",
70
+ "args": [
71
+ "run",
72
+ "-i",
73
+ "--rm",
74
+ "ghcr.io/your-org/<%= name.underscore %>:latest"
75
+ ]
76
+ }
77
+ }
78
+ }
79
+ ```
80
+
81
+ For local development without Docker:
82
+
83
+ ```json
84
+ {
85
+ "mcp": {
86
+ "servers": {
87
+ "<%= name.underscore %>": {
88
+ "command": "plasma",
89
+ "args": [
90
+ "server",
91
+ "/path/to/<%= name.underscore %>"
92
+ ]
93
+ }
94
+ }
95
+ }
96
+ }
97
+ ```
98
+
99
+ More about using MCP server tools in VS Code's [agent mode documentation](https://code.visualstudio.com/docs/editor/agent-mode).
100
+
101
+ ## Setup
102
+
103
+ 1. Install dependencies:
104
+
105
+ ```bash
106
+ bundle install
107
+ ```
108
+
109
+ 2. Configure environment variables in `.env` file
110
+
111
+ ## Running the Server
112
+
113
+ To start the MCP server:
114
+
115
+ ```bash
116
+ plasma server
117
+ ```
118
+
119
+ ## Authentication
120
+
121
+ To run the local authentication system:
122
+
123
+ ```bash
124
+ plasma auth
125
+ ```
126
+
127
+ ## Project Structure
128
+
129
+ ```
130
+ <%= name %>/
131
+ ├── app/
132
+ │ ├── prompts/ # MCP prompts
133
+ │ ├── resources/ # MCP resources
134
+ │ ├── tools/ # MCP tools
135
+ │ ├── variables/ # Per-session variables
136
+ │ └── records/ # Stored objects
137
+ ├── config/
138
+ │ ├── initializers/ # Preload configuration
139
+ │ ├── application.rb # MCP server configuration
140
+ │ └── boot.rb # Launch ignition sequence
141
+ └── .env # Environment variables
142
+ ```
143
+
144
+ ## Adding New Components
145
+
146
+ ### Adding a new Tool
147
+
148
+ Generate a new tool using the CLI:
149
+
150
+ ```bash
151
+ plasma g tool my_tool name:string description:string
152
+ ```
153
+
154
+ This will generate a tool file in `app/tools/my_tool.rb` that follows this structure:
155
+
156
+ ```ruby
157
+ # app/tools/my_tool.rb
158
+ module <%= name.camelize %>
159
+ module Tools
160
+ class MyTool < Plasma::Tool
161
+ param :name,
162
+ type: :string,
163
+ description: "Name parameter description"
164
+
165
+ param :description,
166
+ type: :string,
167
+ description: "Description parameter description"
168
+
169
+ def call
170
+ respond_with(:text,
171
+ text: "Your tool response here"
172
+ )
173
+ end
174
+ end
175
+ end
176
+ end
177
+ ```
178
+
179
+ ### Adding a new Prompt
180
+
181
+ Generate a new prompt using the CLI:
182
+
183
+ ```bash
184
+ plasma g prompt my_prompt
185
+ ```
186
+
187
+ This will generate a prompt file in `app/prompts/my_prompt.rb`:
188
+
189
+ ```ruby
190
+ # app/prompts/my_prompt.rb
191
+ module <%= name.camelize %>
192
+ module Prompts
193
+ class MyPrompt
194
+ def self.system_prompt
195
+ <<~PROMPT
196
+ Your system prompt content here.
197
+ PROMPT
198
+ end
199
+ end
200
+ end
201
+ end
202
+ ```
203
+
204
+ ### Adding a new Resource
205
+
206
+ Generate a new resource using the CLI:
207
+
208
+ ```bash
209
+ plasma g resource my_resource
210
+ ```
211
+
212
+ ### Adding a new Variable
213
+
214
+ Generate a new variable using the CLI:
215
+
216
+ ```bash
217
+ plasma g variable my_variable
218
+ ```
219
+
220
+ ## Interactive Console
221
+
222
+ To start an interactive console with your plasma-mcp application loaded:
223
+
224
+ ```bash
225
+ plasma console
226
+ ```
227
+
228
+ This will give you access to your project's environment where you can interact with your components, storage variables, and other features.
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module <%= module_name %>
4
+ module Prompts
5
+ class <%= name.camelize %> < Plasma::Prompt
6
+ # Define the system prompt content
7
+ with_metadata do
8
+ {
9
+ name: "<%= name.underscore %>",
10
+ description: "A <%= name.underscore %> prompt",
11
+ arguments: <%= parameters.map { |name, type| { name: name, description: "The #{name} parameter", required: true } }.to_json %>
12
+ }
13
+ end
14
+
15
+ def call
16
+ messages = [
17
+ {
18
+ role: "user",
19
+ content: {
20
+ type: "text",
21
+ text: "Process these parameters: <%= parameters.keys.map { |p| "#{p}: #{p}" }.join(", ") %>"
22
+ }
23
+ }
24
+ ]
25
+
26
+ MessageResponse[message: messages]
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module <%= module_name %>
4
+ module Resources
5
+ class <%= name.camelize %> < Plasma::Resource
6
+ # Required by MCP - defines the resource schema
7
+ with_metadata do
8
+ {
9
+ name: "<%= name.underscore %>",
10
+ description: "A <%= name.underscore %> resource",
11
+ content_schema: <%= parameter_schema.to_json %>
12
+ }
13
+ end
14
+
15
+ # Required by MCP - defines the resource content
16
+ def content
17
+ {
18
+ id: id,
19
+ <%= parameters.map { |name, type| "#{name}: #{name}" }.join(",\n ") %>
20
+ }
21
+ end
22
+
23
+ attr_reader :id<%= parameters.map { |name, type| ", :#{name}" }.join %>
24
+
25
+ def initialize(id:, <%= parameters.map { |name, type| "#{name}:" }.join(", ") %>)
26
+ @id = id
27
+ <%= parameters.map { |name, type| "@#{name} = #{name}" }.join("\n ") %>
28
+ super
29
+ end
30
+
31
+ def call
32
+ TextResponse[text: "Here's the data"]
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module <%= module_name %>
4
+ module Tools
5
+ # A <%= kebab_name %> tool
6
+ class <%= name.camelize %>Tool < Plasma::Tool
7
+ <% params.each do |name, type| %>
8
+ param :<%= name %>, type: <%= type.capitalize %>, description: "<%= name.capitalize %> parameter", required: true
9
+ <% end %>
10
+
11
+ # Required, implements the tool functionality
12
+ def call
13
+ # Example of different response types:
14
+
15
+ # Text response with parameters
16
+ respond_with(:text, text: "Hello from <%= name.camelize %>Tool with params: <% params.each do |name, type| %>#{params[:<%= name %>]} <% end %>")
17
+
18
+ # Image response with mime type
19
+ # respond_with(:image, data: image_data, mime_type: "image/png")
20
+
21
+ # Image response without mime type
22
+ # respond_with(:image, data: image_data)
23
+
24
+ # Resource response with mime type
25
+ # respond_with(:resource, mime_type: "text/html", text: "Resource content", uri: "https://example.com")
26
+
27
+ # Resource response without mime type
28
+ # respond_with(:resource, text: "Resource content", uri: "https://example.com")
29
+
30
+ # Error response
31
+ # respond_with(:error, text: "An error occurred")
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module <%= module_name %>
4
+ module Variables
5
+ # A <%= name.underscore %> variable
6
+ class <%= name.camelize %> < Plasma::Storage::Variable
7
+ default <%= default_value %>
8
+
9
+ <% operations.each do |op| %>
10
+ def self.<%= op[:name] %>(<%= op[:params].join(", ") %>)
11
+ self.set(self.get <%= op[:operator] %> <%= op[:params].first %>)
12
+ end
13
+ <% end %>
14
+
15
+ def self.clear
16
+ self.set(<%= default_value %>)
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "plasma"
4
+ require_relative "../lib/<%= name %>/version"
5
+
6
+ module <%= name.camelize %>
7
+ class Application < Plasma::Application
8
+ # Configure your application here
9
+ self.initialize! do |config|
10
+ # Name will be automatically set as "<%= name.camelize %> MCP Server" if not set
11
+ # config.name = "Custom Application Name"
12
+ config.module_name = "<%= name.camelize %>"
13
+
14
+ config.version = <%= name.camelize %>::VERSION
15
+ config.enable_log = true
16
+
17
+ # Example of requiring an environment variable (uncomment to use)
18
+ # config.require_environment_variable("API_KEY")
19
+
20
+ # Example of setting an environment variable (uncomment to use)
21
+ # config.set_environment_variable("DEBUG_MODE", "true")
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Initial ignition sequence
4
+ require "bundler/setup"
5
+ Bundler.require(:default, ENV["PLASMA_ENV"] || "development")
6
+
7
+ # Load environment variables from .env file
8
+ require "dotenv"
9
+ Dotenv.load
10
+
11
+ # Load initializers
12
+ Dir[File.join(__dir__, "initializers/**/*.rb")].sort.each { |f| require f }
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ # This is an example initializer file.
4
+ # Initializers are loaded after the application boots and can be used to
5
+ # configure your MCP server or set up any necessary dependencies.
6
+
7
+ # Add your initialization code here
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module <%= name.camelize %>
4
+ VERSION = "0.0.1"
5
+ end
@@ -0,0 +1,131 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "model_context_protocol"
4
+ require "json"
5
+
6
+ module Plasma
7
+ # Base tool class for PLASMA applications
8
+ class Tool < ModelContextProtocol::Server::Tool
9
+ class << self
10
+ def param(name, type:, description: nil, required: false)
11
+ @params ||= {}
12
+ @params[name.to_sym] = {
13
+ type: type.to_s.downcase,
14
+ description:,
15
+ required:
16
+ }
17
+ end
18
+
19
+ def defined_params
20
+ @params || {}
21
+ end
22
+
23
+ def inherited(subclass)
24
+ super
25
+ const_name = subclass.to_s.split("::").last
26
+ Plasma.const_set(const_name, subclass) unless Plasma.const_defined?(const_name)
27
+ end
28
+
29
+ def metadata
30
+ {
31
+ name: to_s.split("::").last.gsub("Tool", "").underscore,
32
+ description: extract_description,
33
+ inputSchema: {
34
+ type: "object",
35
+ properties: defined_params.transform_values { |v| v.slice(:type, :description).compact },
36
+ required: defined_params.select { |_, v| v[:required] }.keys.map(&:to_s)
37
+ }
38
+ }
39
+ end
40
+
41
+ def extract_description
42
+ return @description if @description
43
+
44
+ @description = read_comments_from_file(file_path).presence || "No description available"
45
+ end
46
+
47
+ def file_path
48
+ loader = Zeitwerk::Registry.loaders.first
49
+ module_name = to_s.split("::")[0..-2].join("::").constantize
50
+ class_name = to_s.split("::").last.to_sym
51
+ path = loader.instance_variable_get(:@inceptions).instance_variable_get(:@map)[module_name][class_name]
52
+ return nil unless path
53
+
54
+ File.expand_path(path)
55
+ end
56
+
57
+ def read_comments_from_file(file_path)
58
+ return "No description available" unless file_path
59
+
60
+ File.readlines(file_path)
61
+ .drop_while { |line| line.include?("frozen_string_literal") || line.strip.empty? }
62
+ .drop_while { |line| line.strip.start_with?("module") }
63
+ .take_while { |line| line.strip.start_with?("#") }
64
+ .join
65
+ .strip
66
+ .gsub(/^#\s*/, "")
67
+ end
68
+ end
69
+
70
+ # rubocop:disable Lint/MissingSuper
71
+ # DO NOT call super as we are deliberately decoupling the tool from the underlying library
72
+ def initialize(params)
73
+ @raw_params = params
74
+ @params = coerce_and_validate!(params)
75
+ end
76
+ # rubocop:enable Lint/MissingSuper
77
+
78
+ attr_reader :params
79
+
80
+ private
81
+
82
+ def coerce_and_validate!(input)
83
+ coerced = {}
84
+ self.class.defined_params.each do |key, config|
85
+ value = input[key.to_s]
86
+ raise ArgumentError, "Missing required parameter: #{key}" if config[:required] && value.nil?
87
+
88
+ coerced[key] = coerce_value(value, config[:type])
89
+ end
90
+ coerced
91
+ end
92
+
93
+ # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
94
+ # This method is deliberately complex as it handles type coercion and validation
95
+ def coerce_value(value, type)
96
+ return if value.nil?
97
+
98
+ case type.to_s.downcase.to_sym
99
+ when :integer then begin
100
+ Integer(value)
101
+ rescue StandardError
102
+ raise ArgumentError, "Invalid integer: #{value}"
103
+ end
104
+ when :float then begin
105
+ Float(value)
106
+ rescue StandardError
107
+ raise ArgumentError, "Invalid float: #{value}"
108
+ end
109
+ when :boolean then !!(value == true || value.to_s.downcase == "true")
110
+ when :string then value.to_s
111
+ when :array
112
+ if value.is_a?(Array)
113
+ value
114
+ elsif value.is_a?(String)
115
+ begin
116
+ parsed = JSON.parse(value)
117
+ raise ArgumentError, "Invalid array: #{value}" unless parsed.is_a?(Array)
118
+
119
+ parsed
120
+ rescue JSON::ParserError
121
+ raise ArgumentError, "Invalid array: #{value}"
122
+ end
123
+ else
124
+ raise ArgumentError, "Invalid array: #{value}"
125
+ end
126
+ else value
127
+ end
128
+ end
129
+ # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
130
+ end
131
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Plasma
4
+ VERSION = "0.0.1"
5
+ end
data/lib/plasma.rb ADDED
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "plasma/version"
4
+ require_relative "plasma/application"
5
+ require_relative "plasma/generator"
6
+ require_relative "plasma/component_generator"
7
+ require_relative "plasma/server"
8
+ require_relative "plasma/auth"
9
+ require_relative "plasma/loader"
10
+ require_relative "plasma/cli"
11
+ require_relative "plasma/prompt"
12
+ require_relative "plasma/resource"
13
+ require_relative "plasma/tool"
14
+ require_relative "plasma/storage"
15
+
16
+ module Plasma
17
+ class Error < StandardError; end
18
+ # Your code goes here...
19
+ end
data/sig/plasma.rbs ADDED
@@ -0,0 +1,4 @@
1
+ module Plasma
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end