zeromcp 0.2.2 → 0.3.0

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b2e5b203cdc7f03afff0cf6ce507e9480455b3f16e469222d5934af9351ffa9a
4
- data.tar.gz: 8e968d22d810a2f25962eb5de0f242438f3ea4359c40cee4616398280695c6f8
3
+ metadata.gz: aa95cc5fe08e2a54b9034a2248b978e1a01919bd3cb0249f444031e070f0dcd1
4
+ data.tar.gz: ae4cd9f4405f8b9786f0bf5e238898b8260d5d37b5a8aac9bdd96ac3ae02876a
5
5
  SHA512:
6
- metadata.gz: b5af7807f278f6430cd65fad75104d90536fbe5c840a2dfce3b7c125415940d65f4166e420f06b085c1ff6fe60c90a525773956e4e341c3b20098258227296a5
7
- data.tar.gz: 3c0c7c8350efedaf3bf7ec4342e85f817b6da45e6668d6aace1553ef98edc478143db42acc4652b119a179c7b4580ddef29c54226457d03b3ed88d4582e6bd09
6
+ metadata.gz: 38b41336ef5617f74cf599c79cb0fd52f872eba7c912825c74020a3026cf8c8e360a1b79ac2f12a612e73cb657e2a2d240a4fed709012796c430990100ddd26f
7
+ data.tar.gz: b502b28ab22e0666937aeb772818d6bf8739671c4b274b03acec2307652d64830aa37ee7611094405be58de2c9d134ed67a3ed507241cb8615ea6fe3c90d7def
data/README.md CHANGED
@@ -43,6 +43,11 @@ ZeroMCP doesn't own the HTTP layer. You bring your own framework; ZeroMCP gives
43
43
  ```ruby
44
44
  require 'sinatra'
45
45
  require 'json'
46
+ require_relative 'lib/zeromcp'
47
+
48
+ config = ZeroMcp::Config.load
49
+ server = ZeroMcp::Server.new(config)
50
+ server.load_tools
46
51
 
47
52
  post '/mcp' do
48
53
  request_body = JSON.parse(request.body.read)
@@ -66,7 +71,7 @@ end
66
71
 
67
72
  ```sh
68
73
  gem build zeromcp.gemspec
69
- gem install zeromcp-0.1.0.gem
74
+ gem install zeromcp-0.2.2.gem
70
75
  ```
71
76
 
72
77
  ## Sandbox
@@ -99,5 +104,5 @@ tools/
99
104
  ## Testing
100
105
 
101
106
  ```sh
102
- ruby -I lib -I test -e 'Dir["test/**/*_test.rb"].each { |f| require_relative f }'
107
+ ruby -I lib -I test -e 'Dir["test/**/test_*.rb"].each { |f| require_relative f }'
103
108
  ```
@@ -7,7 +7,7 @@ module ZeroMcp
7
7
  class Config
8
8
  attr_reader :tools_dir, :resources_dir, :prompts_dir,
9
9
  :separator, :logging, :bypass_permissions, :execute_timeout,
10
- :page_size, :icon
10
+ :page_size, :icon, :title
11
11
 
12
12
  def initialize(opts = {})
13
13
  tools = opts[:tools_dir] || opts['tools'] || './tools'
@@ -28,6 +28,7 @@ module ZeroMcp
28
28
  @namespacing = opts[:namespacing] || opts['namespacing'] || {}
29
29
  @page_size = opts[:page_size] || opts['page_size'] || 0
30
30
  @icon = opts[:icon] || opts['icon']
31
+ @title = opts[:title] || opts['title'] || 'ZeroMCP'
31
32
  end
32
33
 
33
34
  attr_reader :credentials, :cache_credentials, :namespacing
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ module ZeroMcp
6
+ module Credentials
7
+ module_function
8
+
9
+ def resolve(tool_name, config, cache: nil)
10
+ return nil if config.credentials.empty?
11
+ config.credentials.each do |ns, source|
12
+ if tool_name.start_with?("#{ns}_") || tool_name.start_with?("#{ns}#{config.separator}")
13
+ return resolve_for_ns(ns.to_s, source, config, cache)
14
+ end
15
+ end
16
+ nil
17
+ end
18
+
19
+ def resolve_for_ns(ns, source, config, cache)
20
+ return resolve_source(source) unless config.cache_credentials
21
+ return cache[ns] if cache && cache.key?(ns)
22
+ creds = resolve_source(source)
23
+ cache[ns] = creds if cache
24
+ creds
25
+ end
26
+
27
+ def resolve_source(source)
28
+ source = source.transform_keys(&:to_s) if source.is_a?(Hash)
29
+ if source['env']
30
+ val = ENV[source['env']]
31
+ return nil if val.nil? || val.empty?
32
+ begin; return JSON.parse(val); rescue; return val; end
33
+ end
34
+ if source['file']
35
+ path = File.expand_path(source['file'])
36
+ return nil unless File.exist?(path)
37
+ val = File.read(path).strip
38
+ begin; return JSON.parse(val); rescue; return val; end
39
+ end
40
+ nil
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,119 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'schema'
4
+
5
+ module ZeroMcp
6
+ module OpenApi
7
+ module_function
8
+
9
+ def build(tools, config)
10
+ paths = {}
11
+
12
+ tools.each do |_name, tool|
13
+ next unless tool.route.is_a?(Hash)
14
+
15
+ route_method = tool.route_method
16
+ route_path = tool.route_path
17
+
18
+ # Extract :param names from path, convert to {param} for OpenAPI
19
+ path_param_names = route_path.scan(/:([A-Za-z_][A-Za-z0-9_]*)/).flatten
20
+ openapi_path = route_path.gsub(/:([A-Za-z_][A-Za-z0-9_]*)/, '{\1}')
21
+
22
+ input = tool.input || {}
23
+ operation = {
24
+ 'operationId' => tool.name,
25
+ 'description' => tool.description || '',
26
+ 'responses' => {
27
+ '200' => { 'description' => 'Success' },
28
+ '500' => { 'description' => 'Error' }
29
+ }
30
+ }
31
+
32
+ if route_method == 'GET'
33
+ operation['parameters'] = build_parameters(input, path_param_names)
34
+ else
35
+ operation['requestBody'] = build_request_body(input, path_param_names)
36
+ unless path_param_names.empty?
37
+ operation['parameters'] = path_param_names.map do |name|
38
+ {
39
+ 'name' => name,
40
+ 'in' => 'path',
41
+ 'required' => true,
42
+ 'schema' => { 'type' => 'string' }
43
+ }
44
+ end
45
+ end
46
+ end
47
+
48
+ paths[openapi_path] ||= {}
49
+ paths[openapi_path][route_method.downcase] = operation
50
+ end
51
+
52
+ {
53
+ 'openapi' => '3.0.0',
54
+ 'info' => { 'title' => config.title, 'version' => '0.5.0' },
55
+ 'paths' => paths
56
+ }
57
+ end
58
+
59
+ def build_parameters(input, path_param_names)
60
+ params = []
61
+
62
+ # Path params first (preserve order from path)
63
+ path_param_names.each do |name|
64
+ spec = field_to_openapi_schema(input[name] || input[name.to_sym])
65
+ params << {
66
+ 'name' => name,
67
+ 'in' => 'path',
68
+ 'required' => true,
69
+ 'schema' => spec
70
+ }
71
+ end
72
+
73
+ # Remaining fields as query params
74
+ input.each do |key, value|
75
+ key_s = key.to_s
76
+ next if path_param_names.include?(key_s)
77
+
78
+ spec = field_to_openapi_schema(value)
79
+ optional = value.is_a?(Hash) && (value[:optional] || value['optional'])
80
+ params << {
81
+ 'name' => key_s,
82
+ 'in' => 'query',
83
+ 'required' => !optional,
84
+ 'schema' => spec
85
+ }
86
+ end
87
+
88
+ params
89
+ end
90
+
91
+ def build_request_body(input, path_param_names)
92
+ body_input = input.reject { |key, _| path_param_names.include?(key.to_s) }
93
+ schema = Schema.to_json_schema(body_input)
94
+ {
95
+ 'required' => true,
96
+ 'content' => {
97
+ 'application/json' => { 'schema' => schema }
98
+ }
99
+ }
100
+ end
101
+
102
+ def field_to_openapi_schema(value)
103
+ return { 'type' => 'string' } if value.nil?
104
+
105
+ if value.is_a?(String)
106
+ Schema::TYPE_MAP[value] || { 'type' => 'string' }
107
+ elsif value.is_a?(Hash)
108
+ type = value[:type] || value['type']
109
+ mapped = Schema::TYPE_MAP[type.to_s] || { 'type' => 'string' }
110
+ spec = mapped.dup
111
+ desc = value[:description] || value['description']
112
+ spec['description'] = desc if desc
113
+ spec
114
+ else
115
+ { 'type' => 'string' }
116
+ end
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'server'
4
+ require_relative 'openapi'
5
+ require_relative 'credentials'
6
+
7
+ module ZeroMcp
8
+ module Registry
9
+ RouteDefinition = Struct.new(:name, :method, :path, :tool, keyword_init: true)
10
+
11
+ ToolRegistry = Struct.new(:routes, :openapi, :mcp, :config, :credential_cache, keyword_init: true) do
12
+ # Build a ready-to-use Context for `tool`, using the same config-driven
13
+ # credential/permission rules Server#call_tool already applies — so a
14
+ # caller invoking a routed tool directly (route.tool.call(args, ctx))
15
+ # doesn't have to hand-roll credential lookup.
16
+ def context_for(tool)
17
+ ZeroMcp::Context.new(
18
+ tool_name: tool.name,
19
+ permissions: tool.permissions,
20
+ bypass: config.bypass_permissions,
21
+ credentials: ZeroMcp::Credentials.resolve(tool.name, config, cache: credential_cache)
22
+ )
23
+ end
24
+ end
25
+
26
+ module_function
27
+
28
+ # tools: Hash[String, ZeroMcp::Tool] — already-constructed Tool
29
+ # instances, the same object Scanner#load_tool produces.
30
+ # Not raw hashes, and not file paths.
31
+ # config: optional ZeroMcp::Config. Defaults to ZeroMcp::Config.new
32
+ # (in-memory defaults, no zeromcp.config.json file read).
33
+ # When supplied, title:/execute_timeout: are ignored (the
34
+ # config already fully specifies them).
35
+ # title: OpenAPI info.title override. Ignored if config: is given.
36
+ # execute_timeout: default per-tool execute timeout in seconds. Ignored if
37
+ # config: is given.
38
+ def create(tools, config: nil, title: nil, execute_timeout: nil)
39
+ cfg = config || Config.new(title: title, execute_timeout: execute_timeout)
40
+ server = Server.new(cfg, tools: tools)
41
+ credential_cache = {}
42
+
43
+ routes = tools.each_with_object([]) do |(name, tool), acc|
44
+ next unless tool.route.is_a?(Hash)
45
+ acc << RouteDefinition.new(name: name, method: tool.route_method, path: tool.route_path, tool: tool)
46
+ end
47
+
48
+ ToolRegistry.new(
49
+ routes: routes,
50
+ openapi: OpenApi.build(tools, cfg),
51
+ mcp: ->(request) { server.handle_request(request) },
52
+ config: cfg,
53
+ credential_cache: credential_cache
54
+ )
55
+ end
56
+ end
57
+
58
+ # Top-level convenience, mirroring ZeroMcp.serve / ZeroMcp.serve_http.
59
+ def self.create_registry(tools, config: nil, title: nil, execute_timeout: nil)
60
+ Registry.create(tools, config: config, title: title, execute_timeout: execute_timeout)
61
+ end
62
+ end
@@ -57,7 +57,8 @@ module ZeroMcp
57
57
  name: name,
58
58
  description: tool_def[:description] || '',
59
59
  input: tool_def[:input] || {},
60
- permissions: tool_def[:permissions] || {}
60
+ permissions: tool_def[:permissions] || {},
61
+ route: tool_def[:route]
61
62
  ) { |args, ctx| tool_def[:execute].call(args, ctx) }
62
63
 
63
64
  $stderr.puts "[zeromcp] Loaded: #{name}"
@@ -103,10 +104,11 @@ module ZeroMcp
103
104
  @definition = {}
104
105
  end
105
106
 
106
- def tool(description: '', permissions: {}, input: {})
107
+ def tool(description: '', permissions: {}, input: {}, route: nil)
107
108
  @definition[:description] = description
108
109
  @definition[:permissions] = permissions
109
110
  @definition[:input] = input
111
+ @definition[:route] = route
110
112
  end
111
113
 
112
114
  def execute(&block)
@@ -3,29 +3,87 @@
3
3
  require 'json'
4
4
  require 'base64'
5
5
  require 'timeout'
6
+ require 'webrick'
6
7
  require_relative 'schema'
7
8
  require_relative 'config'
8
9
  require_relative 'tool'
9
10
  require_relative 'scanner'
10
11
  require_relative 'sandbox'
12
+ require_relative 'openapi'
13
+ require_relative 'credentials'
11
14
 
12
15
  module ZeroMcp
13
16
  class Server
14
- def initialize(config = nil)
17
+ def initialize(config = nil, tools: nil)
15
18
  @config = config || Config.load
16
19
  @scanner = Scanner.new(@config)
17
20
  @resource_scanner = ResourceScanner.new(@config)
18
21
  @prompt_scanner = PromptScanner.new(@config)
19
- @tools = {}
22
+ @tools = tools || {}
20
23
  @resources = {}
21
24
  @templates = {}
22
25
  @prompts = {}
23
26
  @subscriptions = {}
24
27
  @log_level = 'info'
25
- @icon = nil
28
+ @icon = tools ? Config.resolve_icon(@config.icon) : nil
26
29
  @credential_cache = {}
27
30
  end
28
31
 
32
+ # Start an HTTP server that exposes:
33
+ # POST /mcp — JSON-RPC over HTTP
34
+ # <method> <path> — per-tool route handlers (tools with a `route:` field)
35
+ #
36
+ # Usage: ZeroMcp::Server.new.serve_http(port: 3000)
37
+ def serve_http(port: 3000)
38
+ load_tools
39
+
40
+ http = WEBrick::HTTPServer.new(Port: port, Logger: WEBrick::Log.new($stderr), AccessLog: [])
41
+ $stderr.puts "[zeromcp] HTTP server listening on port #{port}"
42
+
43
+ # /mcp — JSON-RPC endpoint
44
+ http.mount_proc('/mcp') do |req, res|
45
+ begin
46
+ request = JSON.parse(req.body || '{}')
47
+ rescue JSON::ParserError
48
+ res.status = 400
49
+ res.content_type = 'application/json'
50
+ res.body = JSON.generate({ 'error' => 'Invalid JSON' })
51
+ next
52
+ end
53
+ response = handle_request(request)
54
+ res.content_type = 'application/json'
55
+ res.body = JSON.generate(response || {})
56
+ end
57
+
58
+ # /openapi.json — auto-generated OpenAPI 3.0 spec from routed tools
59
+ http.mount_proc('/openapi.json') do |_req, res|
60
+ res.content_type = 'application/json'
61
+ res.body = JSON.generate(build_openapi_spec)
62
+ end
63
+
64
+ # /docs — Swagger UI
65
+ http.mount_proc('/docs') do |_req, res|
66
+ res.content_type = 'text/html'
67
+ res.body = SWAGGER_UI_HTML
68
+ end
69
+
70
+ # /health — liveness check
71
+ http.mount_proc('/health') do |_req, res|
72
+ res.content_type = 'application/json'
73
+ res.body = JSON.generate({ 'ok' => true })
74
+ end
75
+
76
+ # Register per-tool HTTP routes
77
+ @tools.each do |_name, tool|
78
+ next unless tool.route.is_a?(Hash)
79
+ register_tool_route(http, tool, tool.route_method, tool.route_path)
80
+ end
81
+
82
+ trap('INT') { http.shutdown }
83
+ trap('TERM') { http.shutdown }
84
+ http.start
85
+ end
86
+
29
87
  # Load tools (and resources/prompts) from the configured directories.
30
88
  # Call this before using handle_request directly (serve calls this
31
89
  # automatically).
@@ -441,37 +499,95 @@ module ZeroMcp
441
499
  end
442
500
 
443
501
  def _resolve_credentials(tool_name)
444
- return nil if @config.credentials.empty?
445
- @config.credentials.each do |ns, source|
446
- if tool_name.start_with?("#{ns}_") || tool_name.start_with?("#{ns}#{@config.separator}")
447
- return _resolve_credentials_for_ns(ns.to_s, source)
502
+ Credentials.resolve(tool_name, @config, cache: @credential_cache)
503
+ end
504
+
505
+ # --- HTTP route helpers ---
506
+
507
+ def register_tool_route(http, tool, route_method, route_path)
508
+ # Convert :param segments to a regex for matching
509
+ param_names = []
510
+ regex_str = route_path.gsub(/:([A-Za-z_][A-Za-z0-9_]*)/) do
511
+ param_names << $1
512
+ '([^/]+)'
513
+ end
514
+ route_regex = /\A#{regex_str}\z/
515
+
516
+ http.mount_proc(route_path_prefix(route_path)) do |req, res|
517
+ next unless req.request_method == route_method
518
+
519
+ path_params = extract_path_params(req.path, route_regex, param_names)
520
+ unless path_params
521
+ res.status = 404
522
+ res.content_type = 'application/json'
523
+ res.body = JSON.generate({ 'ok' => false, 'error' => 'Not found' })
524
+ next
525
+ end
526
+
527
+ args = if route_method == 'GET'
528
+ query_args = WEBrick::HTTPUtils.parse_query(req.query_string || '')
529
+ query_args.merge(path_params)
530
+ else
531
+ body_args = begin
532
+ req.body && !req.body.empty? ? JSON.parse(req.body) : {}
533
+ rescue JSON::ParserError
534
+ {}
535
+ end
536
+ body_args.merge(path_params)
537
+ end
538
+
539
+ begin
540
+ ctx = Context.new(tool_name: tool.name, permissions: tool.permissions,
541
+ bypass: @config.bypass_permissions,
542
+ credentials: _resolve_credentials(tool.name))
543
+ timeout_secs = (tool.permissions.is_a?(Hash) && tool.permissions[:execute_timeout]) ||
544
+ (tool.permissions.is_a?(Hash) && tool.permissions['execute_timeout']) ||
545
+ @config.execute_timeout
546
+ result = Timeout.timeout(timeout_secs) { tool.call(args, ctx) }
547
+ res.content_type = 'application/json'
548
+ res.body = JSON.generate({ 'ok' => true, 'result' => result })
549
+ rescue => e
550
+ res.status = 500
551
+ res.content_type = 'application/json'
552
+ res.body = JSON.generate({ 'ok' => false, 'error' => e.message })
448
553
  end
449
554
  end
450
- nil
451
555
  end
452
556
 
453
- def _resolve_credentials_for_ns(ns, source)
454
- return _resolve_credential_source(source) unless @config.cache_credentials
455
- return @credential_cache[ns] if @credential_cache.key?(ns)
456
- creds = _resolve_credential_source(source)
457
- @credential_cache[ns] = creds
458
- creds
557
+ def route_path_prefix(route_path)
558
+ # WEBrick mounts on a fixed prefix; use the static prefix before first :param
559
+ route_path.split('/:').first || '/'
459
560
  end
460
561
 
461
- def _resolve_credential_source(source)
462
- source = source.transform_keys(&:to_s) if source.is_a?(Hash)
463
- if source['env']
464
- val = ENV[source['env']]
465
- return nil if val.nil? || val.empty?
466
- begin; return JSON.parse(val); rescue; return val; end
467
- end
468
- if source['file']
469
- path = File.expand_path(source['file'])
470
- return nil unless File.exist?(path)
471
- val = File.read(path).strip
472
- begin; return JSON.parse(val); rescue; return val; end
473
- end
474
- nil
562
+ def extract_path_params(path, route_regex, param_names)
563
+ m = path.match(route_regex)
564
+ return nil unless m
565
+ result = {}
566
+ param_names.each_with_index { |name, i| result[name] = m[i + 1] }
567
+ result
475
568
  end
569
+
570
+ # --- OpenAPI spec builder ---
571
+
572
+ def build_openapi_spec
573
+ OpenApi.build(@tools, @config)
574
+ end
575
+
576
+ SWAGGER_UI_HTML = <<~HTML.freeze
577
+ <!DOCTYPE html>
578
+ <html>
579
+ <head>
580
+ <title>ZeroMCP API</title>
581
+ <meta charset="utf-8"/>
582
+ <meta name="viewport" content="width=device-width, initial-scale=1">
583
+ <link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist/swagger-ui.css">
584
+ </head>
585
+ <body>
586
+ <div id="swagger-ui"></div>
587
+ <script src="https://unpkg.com/swagger-ui-dist/swagger-ui-bundle.js"></script>
588
+ <script>SwaggerUIBundle({ url: '/openapi.json', dom_id: '#swagger-ui' })</script>
589
+ </body>
590
+ </html>
591
+ HTML
476
592
  end
477
593
  end
data/lib/zeromcp/tool.rb CHANGED
@@ -4,13 +4,14 @@ require_relative 'schema'
4
4
 
5
5
  module ZeroMcp
6
6
  class Tool
7
- attr_reader :name, :description, :input, :permissions, :execute_block, :cached_schema
7
+ attr_reader :name, :description, :input, :permissions, :execute_block, :cached_schema, :route
8
8
 
9
- def initialize(name:, description: '', input: {}, permissions: {}, &block)
9
+ def initialize(name:, description: '', input: {}, permissions: {}, route: nil, &block)
10
10
  @name = name
11
11
  @description = description
12
12
  @input = input
13
13
  @permissions = permissions
14
+ @route = route
14
15
  @execute_block = block
15
16
  @cached_schema = Schema.to_json_schema(@input)
16
17
  end
@@ -18,6 +19,16 @@ module ZeroMcp
18
19
  def call(args, ctx = {})
19
20
  @execute_block.call(args, ctx)
20
21
  end
22
+
23
+ def route_method
24
+ return nil unless @route.is_a?(Hash)
25
+ (@route[:method] || @route['method'] || 'POST').upcase
26
+ end
27
+
28
+ def route_path
29
+ return nil unless @route.is_a?(Hash)
30
+ @route[:path] || @route['path'] || '/'
31
+ end
21
32
  end
22
33
 
23
34
  class Context
data/lib/zeromcp.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative 'zeromcp/server'
4
+ require_relative 'zeromcp/registry'
4
5
 
5
6
  module ZeroMcp
6
7
  def self.serve(config_path = nil)
@@ -8,4 +9,10 @@ module ZeroMcp
8
9
  server = Server.new(config)
9
10
  server.serve
10
11
  end
12
+
13
+ def self.serve_http(port: 3000, config_path: nil)
14
+ config = config_path ? Config.load(config_path) : Config.load
15
+ server = Server.new(config)
16
+ server.serve_http(port: port)
17
+ end
11
18
  end
data/zeromcp.gemspec CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Gem::Specification.new do |s|
4
4
  s.name = 'zeromcp'
5
- s.version = '0.2.2'
5
+ s.version = '0.3.0'
6
6
  s.summary = 'Zero-config MCP runtime'
7
7
  s.description = 'Drop tool files in a directory, get a working MCP server. Zero boilerplate.'
8
8
  s.authors = ['Antidrift']
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: zeromcp
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.2
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Antidrift
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-04-25 00:00:00.000000000 Z
11
+ date: 2026-08-06 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: Drop tool files in a directory, get a working MCP server. Zero boilerplate.
14
14
  email: hello@probeo.io
@@ -22,6 +22,9 @@ files:
22
22
  - bin/zeromcp
23
23
  - lib/zeromcp.rb
24
24
  - lib/zeromcp/config.rb
25
+ - lib/zeromcp/credentials.rb
26
+ - lib/zeromcp/openapi.rb
27
+ - lib/zeromcp/registry.rb
25
28
  - lib/zeromcp/sandbox.rb
26
29
  - lib/zeromcp/scanner.rb
27
30
  - lib/zeromcp/schema.rb