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
data/lib/plasma/cli.rb ADDED
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "thor"
4
+ require "fileutils"
5
+ require "zeitwerk"
6
+
7
+ module Plasma
8
+ # CLI interface for the plasma-mcp gem
9
+ class CLI < Thor
10
+ desc "new NAME", "Create a new MCP server project"
11
+ method_option :skip_git, type: :boolean, default: false, desc: "Skip Git initialization"
12
+ def new(name)
13
+ generator = Generator.new(name, options)
14
+ generator.generate
15
+
16
+ return if options[:skip_git]
17
+
18
+ Dir.chdir(name) do
19
+ system("git init .")
20
+ end
21
+ end
22
+
23
+ desc "server [/path/to/your/plasma/project]", "Launch your MCP server from the specified project path"
24
+ def server(path = nil)
25
+ # Change to the specified path if provided
26
+ Dir.chdir(path) if path
27
+
28
+ # Start the server
29
+ server = Server.new(options)
30
+ server.start
31
+ end
32
+
33
+ # Alias for server
34
+ map "s" => :server
35
+
36
+ desc "auth", "Launch the local authentication beacon"
37
+ method_option :port, type: :numeric, desc: "Port to run the auth server on"
38
+ method_option :host, type: :string, desc: "Host to bind the auth server to"
39
+ def auth
40
+ puts "Engaging local authentication system..."
41
+
42
+ # Start the auth server with options
43
+ auth = Auth.new(options)
44
+ auth.start
45
+ end
46
+
47
+ desc "console", "Start an interactive console with your PLASMA application loaded"
48
+ def console
49
+ require "debug"
50
+
51
+ # Load the application so we can use it in the console if we need to
52
+ require_relative "loader"
53
+ _application = Plasma::Loader.load_project
54
+
55
+ # Start an interactive console
56
+ binding.irb # rubocop:disable Lint/Debugger
57
+ end
58
+
59
+ desc "version", "Show PLASMA version"
60
+ def version
61
+ puts "PLASMA version #{Plasma::VERSION}"
62
+ end
63
+
64
+ # Generator commands
65
+ desc "generate TYPE NAME [PARAMETERS]", "Generate a new component (tool, prompt, resource, or variable)"
66
+ def generate(type, name, *parameters)
67
+ # Parse parameters into a hash
68
+ params = {}
69
+ parameters.each do |param|
70
+ key, value = param.split(":")
71
+ params[key] = value if key && value
72
+ end
73
+
74
+ # Create the generator
75
+ generator = ComponentGenerator.new(type, name, params)
76
+ generator.generate
77
+ end
78
+
79
+ # Alias for generate
80
+ map "g" => :generate
81
+
82
+ private
83
+
84
+ def plasma_project?
85
+ File.exist?("config/application.rb") &&
86
+ File.exist?("config/boot.rb") &&
87
+ Dir.exist?("app/prompts") &&
88
+ Dir.exist?("app/resources") &&
89
+ Dir.exist?("app/tools") &&
90
+ Dir.exist?("app/variables")
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+ require "erb"
5
+ require "active_support/inflector"
6
+ require "json"
7
+
8
+ module Plasma
9
+ # Handles generating individual MCP components (tools, prompts, resources, variables)
10
+ class ComponentGenerator
11
+ attr_reader :component_type, :name, :params
12
+
13
+ def initialize(component_type, name, params = {})
14
+ @component_type = component_type
15
+ @name = name
16
+ @params = params
17
+ @template_dir = File.expand_path("templates", __dir__)
18
+ puts "Template directory: #{@template_dir}"
19
+ end
20
+
21
+ def generate
22
+ # Check if we're in a PLASMA project directory
23
+ unless plasma_project?
24
+ puts "Error: Not a PLASMA project directory. Please run this command from the root of a PLASMA project."
25
+ return false
26
+ end
27
+
28
+ create_component_file
29
+ true
30
+ end
31
+
32
+ private
33
+
34
+ def plasma_project?
35
+ File.exist?("config/application.rb") &&
36
+ File.exist?("config/boot.rb") &&
37
+ Dir.exist?("app/prompts") &&
38
+ Dir.exist?("app/resources") &&
39
+ Dir.exist?("app/tools") &&
40
+ Dir.exist?("app/variables")
41
+ end
42
+
43
+ def create_component_file
44
+ template_path = find_template_path
45
+ content = render_template(template_path)
46
+ write_component_file(content)
47
+ end
48
+
49
+ def find_template_path
50
+ path = File.join(@template_dir, "app", "#{@component_type}s", "example_#{@component_type}.rb.erb")
51
+ raise "Template not found at: #{path}" unless File.exist?(path)
52
+
53
+ path
54
+ end
55
+
56
+ def render_template(template_path)
57
+ template = ERB.new(File.read(template_path), trim_mode: "-")
58
+ template.result(binding)
59
+ end
60
+
61
+ def write_component_file(content)
62
+ component_dir = File.join("app", "#{@component_type}s")
63
+ FileUtils.mkdir_p(component_dir)
64
+
65
+ file_path = File.join(component_dir, "#{@name.downcase}_#{@component_type}.rb")
66
+ File.write(file_path, content)
67
+ end
68
+
69
+ def module_name
70
+ # Get the application module name from the current directory
71
+ File.basename(Dir.pwd).gsub("-", "_").camelize
72
+ end
73
+
74
+ def parameter_schema
75
+ return {} if params.empty?
76
+
77
+ schema = {
78
+ type: "object",
79
+ properties: params.transform_values { |type| { type: type } },
80
+ required: params.keys
81
+ }
82
+
83
+ JSON.pretty_generate(schema)
84
+ end
85
+
86
+ # Convert the name to the appropriate format for the file
87
+ def file_name
88
+ # Convert camelCase to snake_case and append _tool
89
+ "#{@name.underscore}_#{@component_type}"
90
+ end
91
+
92
+ # Convert the name to kebab-case for the metadata
93
+ def kebab_name
94
+ @name.underscore.gsub("_", "-")
95
+ end
96
+
97
+ def component_class_name
98
+ @name.split("_").map(&:capitalize).join
99
+ end
100
+
101
+ def component_name
102
+ @name.downcase
103
+ end
104
+
105
+ def parameter_definitions
106
+ @params.map do |name, type|
107
+ "#{name}: { type: \"#{type}\" }"
108
+ end.join(",\n ")
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,132 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+ require "erb"
5
+ require "active_support/inflector"
6
+
7
+ module Plasma
8
+ # Handles generating new MCP server projects from templates
9
+ class Generator
10
+ attr_reader :name, :options
11
+
12
+ def initialize(name, options = {})
13
+ @name = name
14
+ @options = options
15
+ end
16
+
17
+ def generate
18
+ create_project_directory
19
+ create_directory_structure
20
+ generate_files_from_templates
21
+ copy_static_files
22
+ move_version_file
23
+ end
24
+
25
+ private
26
+
27
+ def create_project_directory
28
+ FileUtils.mkdir_p(name)
29
+ end
30
+
31
+ def create_directory_structure
32
+ dirs = %w[app/prompts app/resources app/tools app/variables app/records
33
+ config/initializers lib .github/workflows]
34
+
35
+ dirs.each do |dir|
36
+ dir_path = File.join(name, dir)
37
+ FileUtils.mkdir_p(dir_path)
38
+ FileUtils.touch(File.join(dir_path, ".gitkeep"))
39
+ end
40
+ end
41
+
42
+ def templates_dir
43
+ File.expand_path("templates", __dir__)
44
+ end
45
+
46
+ def move_version_file
47
+ # Create the namespace directory
48
+ namespace_dir = File.join(name, "lib", name.underscore)
49
+ FileUtils.mkdir_p(namespace_dir)
50
+
51
+ # Move the version file to the correct location
52
+ version_file = File.join(name, "lib/version.rb")
53
+ new_version_file = File.join(namespace_dir, "version.rb")
54
+ return unless File.exist?(version_file)
55
+
56
+ FileUtils.mv(version_file, new_version_file)
57
+ end
58
+
59
+ # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
60
+ def generate_files_from_templates
61
+ # Exclude example component templates and include dot files
62
+ template_files = Dir.glob(File.join(templates_dir, "**", "*.erb"), File::FNM_DOTMATCH).reject do |file|
63
+ result = file.include?("/app/") && file.include?("example_")
64
+ result
65
+ end
66
+
67
+ template_files.each do |template_path|
68
+ # Get relative path from templates directory
69
+ relative_path = template_path.sub("#{templates_dir}/", "")
70
+ # Remove .erb extension
71
+ output_path = relative_path.sub(/\.erb$/, "")
72
+ # Replace placeholder with actual project name
73
+ output_path = output_path.gsub("PROJECT_NAME", name)
74
+
75
+ # Create full path for destination
76
+ dest_path = File.join(name, output_path)
77
+
78
+ # Ensure directory exists
79
+ FileUtils.mkdir_p(File.dirname(dest_path))
80
+
81
+ # Render the template
82
+ template = ERB.new(File.read(template_path))
83
+ result = template.result(binding)
84
+
85
+ # Write file
86
+ File.write(dest_path, result)
87
+ end
88
+ end
89
+ # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
90
+
91
+ # rubocop:disable Metrics/AbcSize
92
+ def copy_static_files
93
+ static_files = Dir.glob(File.join(templates_dir, "**", "*"), File::FNM_DOTMATCH).reject do |file|
94
+ File.directory?(file) || file.end_with?(".erb")
95
+ end
96
+
97
+ static_files.each do |file_path|
98
+ relative_path = file_path.sub("#{templates_dir}/", "")
99
+ dest_path = File.join(name, relative_path.gsub("PROJECT_NAME", name))
100
+
101
+ FileUtils.mkdir_p(File.dirname(dest_path))
102
+ FileUtils.cp(file_path, dest_path)
103
+ end
104
+ end
105
+ # rubocop:enable Metrics/AbcSize
106
+
107
+ def module_name
108
+ name.gsub("-", "_").camelize
109
+ end
110
+ end
111
+ end
112
+
113
+ # Add extension methods for string transformations in ERB templates
114
+ class String
115
+ unless method_defined?(:camelize)
116
+ def camelize
117
+ ActiveSupport::Inflector.camelize(self)
118
+ end
119
+ end
120
+
121
+ unless method_defined?(:underscore)
122
+ def underscore
123
+ ActiveSupport::Inflector.underscore(self)
124
+ end
125
+ end
126
+
127
+ unless method_defined?(:dasherize)
128
+ def dasherize
129
+ ActiveSupport::Inflector.dasherize(self)
130
+ end
131
+ end
132
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Plasma
4
+ # Handles loading PLASMA projects
5
+ class Loader
6
+ def self.load_project
7
+ # Load the application boot file
8
+ require_relative File.join(Dir.pwd, "config/boot")
9
+
10
+ # Determine the application class name
11
+ app_name = File.basename(Dir.pwd).gsub("-", "_").camelize
12
+
13
+ # Load the application
14
+ require_relative File.join(Dir.pwd, "config/application")
15
+
16
+ # Get the application module and include it in the top-level scope
17
+ app_module = Object.const_get(app_name)
18
+ Object.include(app_module)
19
+
20
+ # Return the application class
21
+ Object.const_get("#{app_name}::Application")
22
+ rescue LoadError, NameError => e
23
+ handle_load_error(e)
24
+ end
25
+
26
+ def self.handle_load_error(error)
27
+ message = error.is_a?(LoadError) ? "Error loading application" : "Error initializing application"
28
+ puts "#{message}: #{error.message}"
29
+ raise error
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "model_context_protocol"
4
+
5
+ module Plasma
6
+ # Base prompt class for PLASMA applications
7
+ class Prompt < ModelContextProtocol::Server::Prompt
8
+ # For now, this defers to the base class for everything
9
+ end
10
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "model_context_protocol"
4
+
5
+ module Plasma
6
+ # Base resource class for PLASMA applications
7
+ class Resource < ModelContextProtocol::Server::Resource
8
+ # Required by MCP - defines the resource schema
9
+ # rubocop:disable Metrics/MethodLength
10
+ def self.schema
11
+ {
12
+ name: name.underscore,
13
+ description: "Base resource for PLASMA applications",
14
+ content_schema: {
15
+ type: "object",
16
+ properties: {
17
+ id: { type: "string" }
18
+ },
19
+ required: ["id"]
20
+ }
21
+ }
22
+ end
23
+ # rubocop:enable Metrics/MethodLength
24
+
25
+ # Required by MCP - defines the resource content
26
+ def content
27
+ {
28
+ id: id
29
+ }
30
+ end
31
+
32
+ attr_reader :id
33
+
34
+ def initialize(id:)
35
+ @id = id
36
+ super
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "model_context_protocol"
4
+
5
+ module Plasma
6
+ # Handles MCP server operations
7
+ class Server
8
+ attr_reader :options
9
+
10
+ def initialize(options = {})
11
+ @options = options
12
+ end
13
+
14
+ def start
15
+ # Check if we're in a PLASMA project directory
16
+ unless plasma_project?
17
+ puts "Error: Not a PLASMA project directory. Please run this command from the root of a PLASMA project."
18
+ return false
19
+ end
20
+
21
+ # Load the application
22
+ app_class = load_application
23
+
24
+ # Start the MCP server
25
+ app_class.start_server
26
+
27
+ true
28
+ rescue Interrupt
29
+ # puts "\nShutting down MCP server..."
30
+ end
31
+
32
+ private
33
+
34
+ def plasma_project?
35
+ File.exist?("config/application.rb") &&
36
+ File.exist?("config/boot.rb") &&
37
+ Dir.exist?("app/prompts") &&
38
+ Dir.exist?("app/resources") &&
39
+ Dir.exist?("app/tools")
40
+ end
41
+
42
+ def load_application
43
+ require_relative "loader"
44
+ Plasma::Loader.load_project
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Plasma
4
+ module Storage
5
+ # Base class for all application models
6
+ class ApplicationRecord < ActiveRecord::Base
7
+ self.abstract_class = true
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Plasma
4
+ module Storage
5
+ # Model for storing serialized data with automatic JSON parsing
6
+ class Record < ApplicationRecord
7
+ class << self
8
+ def inherited(subclass)
9
+ super
10
+ const_name = subclass.to_s.split("::").last
11
+ Plasma.const_set(const_name, subclass) unless Plasma.const_defined?(const_name)
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Plasma
4
+ module Storage
5
+ # Model for storing key-value pairs with automatic JSON parsing
6
+ class Variable < ApplicationRecord
7
+ self.primary_key = "key"
8
+
9
+ serialize :value do |val|
10
+ case val
11
+ when String
12
+ begin
13
+ JSON.parse(val)
14
+ rescue JSON::ParserError
15
+ val
16
+ end
17
+ when Hash, Array
18
+ val
19
+ else
20
+ val.to_s
21
+ end
22
+ end
23
+
24
+ class << self
25
+ attr_accessor :default_value
26
+
27
+ def inherited(subclass)
28
+ super
29
+ const_name = subclass.to_s.split("::").last
30
+ Plasma.const_set(const_name, subclass) unless Plasma.const_defined?(const_name)
31
+ end
32
+
33
+ def key
34
+ name.demodulize.underscore
35
+ end
36
+
37
+ def set(value)
38
+ raise "Failed to set value for #{key}" unless create_or_find_by(key: key).update(value: value)
39
+
40
+ value
41
+ end
42
+
43
+ def get
44
+ find_by(key: key)&.value || default_value
45
+ end
46
+
47
+ def default(value)
48
+ @default_value = value
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_record"
4
+ require "sqlite3"
5
+
6
+ # Load all models
7
+ Dir[File.join(__dir__, "storage", "*.rb")].each { |file| require file }
8
+
9
+ module Plasma
10
+ # This module handles the storage operations for PLASMA applications
11
+ module Storage
12
+ # rubocop:disable Metrics/MethodLength
13
+ def self.define_default_schema
14
+ ActiveRecord::Schema.define(verbose: false) do
15
+ create_table :records do |t|
16
+ t.json :data
17
+ t.timestamps
18
+ end
19
+
20
+ create_table :variables, id: false do |t|
21
+ t.string :key, primary_key: true
22
+ t.json :value
23
+ end
24
+
25
+ create_table :settings do |t|
26
+ t.string :group, null: false
27
+ t.json :data
28
+ t.timestamps
29
+ end
30
+
31
+ add_index :settings, :group, unique: true
32
+ end
33
+ end
34
+ # rubocop:enable Metrics/MethodLength
35
+
36
+ def self.setup
37
+ establish_connection
38
+ load_schema
39
+ end
40
+
41
+ def self.establish_connection
42
+ ActiveRecord::Base.establish_connection(
43
+ adapter: "sqlite3",
44
+ database: ":memory:"
45
+ )
46
+ ActiveRecord::Migration.verbose = false
47
+ end
48
+
49
+ def self.load_schema
50
+ schema_path = File.join(Dir.pwd, "db", "schema.rb")
51
+ if File.exist?(schema_path)
52
+ load schema_path
53
+ else
54
+ define_default_schema
55
+ end
56
+ end
57
+
58
+ def self.add_record(fields = {})
59
+ Plasma::Storage::Record.create(data: fields)
60
+ end
61
+
62
+ def self.find_records
63
+ Plasma::Storage::Record.all.map { |r| r.data.merge(id: r.id, created_at: r.created_at) }
64
+ end
65
+
66
+ def self.find_records_by_field(field, value)
67
+ Plasma::Storage::Record.all.select { |r| r.data[field.to_s] == value || r.data[field.to_sym] == value }
68
+ .map { |r| r.data.merge(id: r.id, created_at: r.created_at) }
69
+ end
70
+
71
+ def self.find_records_with_field(field)
72
+ records = Plasma::Storage::Record.all
73
+ filtered = records.select { |r| r.data.key?(field.to_s) || r.data.key?(field.to_sym) }
74
+ filtered.map { |r| r.data.merge(id: r.id, created_at: r.created_at) }
75
+ end
76
+
77
+ def self.update_record_field(id, field, value)
78
+ record = Plasma::Storage::Record.find(id)
79
+ record.data[field] = value
80
+ record.save!
81
+ end
82
+
83
+ def self.set_var(key, value)
84
+ Plasma::Storage::Variable.create_or_find_by(key: key).update(value: value)
85
+ end
86
+
87
+ def self.get_var(key)
88
+ Plasma::Storage::Variable.find_by(key: key)&.value
89
+ end
90
+
91
+ def self.dump_to_json
92
+ {
93
+ records: find_records,
94
+ variables: Plasma::Storage::Variable.all.pluck(:key, :value).to_h
95
+ }.to_json
96
+ end
97
+ end
98
+ end
99
+
100
+ # Setup database when module is loaded
101
+ Plasma::Storage.setup
@@ -0,0 +1,40 @@
1
+ # Git
2
+ .git
3
+ .gitignore
4
+
5
+ # Ruby
6
+ *.gem
7
+ *.rbc
8
+ /.config
9
+ /coverage/
10
+ /InstalledFiles
11
+ /pkg/
12
+ /spec/reports/
13
+ /spec/examples.txt
14
+ /test/tmp/
15
+ /test/version_tmp/
16
+ /tmp/
17
+
18
+ # Environment
19
+ .env
20
+ .env.*
21
+ !.env.example
22
+
23
+ # Logs
24
+ *.log
25
+
26
+ # Editor directories and files
27
+ .idea
28
+ .vscode
29
+ *.swp
30
+ *.swo
31
+ *~
32
+
33
+ # OS generated files
34
+ .DS_Store
35
+ .DS_Store?
36
+ ._*
37
+ .Spotlight-V100
38
+ .Trashes
39
+ ehthumbs.db
40
+ Thumbs.db
@@ -0,0 +1,2 @@
1
+ # Environment variables
2
+ PLASMA_ENV=development