cf-mcp 0.18.0 → 0.20.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: 4161d3091e34024cc73f40437b755e9c08b9ed4679e642caf3578dbf24e4eae0
4
- data.tar.gz: 7ca49f9aba9d0cb13e9cfd1c738848792a162a1fb2d97c1413bd713eb9e268a6
3
+ metadata.gz: aa1835b909429755f2b06a50b6db8d973c68445f879413974b7b28863b6bce06
4
+ data.tar.gz: 6a110d101e27e125763506715bed2a92b973e9cecd709bd17a69c7088fb5e55f
5
5
  SHA512:
6
- metadata.gz: 40f8ce2d39d0816d8e0acdd75922a7cf77935dccf4c4a8be3e14084e9cb09ef21ea73beb6ff8d454eae372a22ec542e557137d5e539a8e2671f291f88d8ba005
7
- data.tar.gz: 271d54107c00a36dc2bb2245265d22709752bed02ae5388dc6c8e5ed98ed09f2ba400d070f58d2f5f111ad5c24c6d3c83dde655c84b621bb6f99044aa0ab90e8
6
+ metadata.gz: e450154e426288754d99ee28a63e4b3fa99d2394600ab8519df3f0d584844b2428f64a1c62d106053f3f3826ace1d4e4beeadd6d9f45e0100b2d7bcc3717db69
7
+ data.tar.gz: 7fd889d1f4ae36abed48cb84048efa8debf22a4c97d198c2d12745ad095589a3ab02feeb5c2c1d4d3221f33a38069c363214f3142b7c56bcbf0c30e04d790f54
data/CHANGELOG.md CHANGED
@@ -7,6 +7,45 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.20.0] - 2026-09-19
11
+
12
+ ### Added
13
+
14
+ - `CF_MCP_ALLOWED_HOSTS` environment variable (comma-separated) to allow extra `Host` header values on the HTTP endpoint; set to `cf-mcp.fly.dev` in `fly.toml`
15
+ - Support MCP protocol version 2026-07-28: stateless requests carrying their version in `_meta`, and the `server/discover` method
16
+ - Allow the `MCP-Protocol-Version`, `Mcp-Method` and `Mcp-Name` headers in CORS preflight responses
17
+
18
+ ### Changed
19
+
20
+ - **BREAKING:** Raised the minimum supported Ruby from 3.2 to 3.3 (`required_ruby_version` and the Standard Ruby target); 3.2 reached end of life on 2026-03-31; CI now also runs against Ruby 3.3
21
+ - Updated `mcp` from 0.25.0 to 1.5.1
22
+ - Updated `puma` from 7.2.1 to 8.0.2 (the gemspec constraint is now `~> 8.0`) and `rubyzip` from 3.6.0 to 3.7.0
23
+ - Removed the `2025-03-26` protocol pin; clients using the `initialize` handshake now negotiate up to 2025-11-25
24
+ - The web UI now calls tools through the stateless 2026-07-28 protocol
25
+ - The web UI shows both the modern and the legacy handshake protocol versions
26
+ - Bump Ruby to 4.0.7 in `.ruby-version`, the Dockerfile, and the CI matrix
27
+ - Bumped GitHub Actions: `actions/checkout` to v7 and `actions/cache` to v6
28
+ - Added least-privilege permissions, PR concurrency cancellation and a job timeout to the CI workflow, and Dependabot for GitHub Actions and Bundler
29
+
30
+ ### Fixed
31
+
32
+ - HTTP endpoint rejected all non-loopback requests with `403 Invalid Host header` since `mcp` 0.25 enabled DNS-rebinding protection, breaking the hosted server
33
+
34
+ ## [0.19.0] - 2026-09-15
35
+
36
+ ### Added
37
+
38
+ - Show the Cute Framework revision (git commit SHA) being served at startup and on the dashboard, linked to the commit on GitHub
39
+ - Print progress while parsing header files, when running in a terminal
40
+
41
+ ### Changed
42
+
43
+ - Updated dependencies, including `mcp` from 0.22.0 to 0.25.0
44
+
45
+ ### Fixed
46
+
47
+ - `--root` now accepts a Cute Framework project root (as documented) and auto-descends into its `include/` directory; previously this scanned unrelated files and skipped topic indexing entirely
48
+
10
49
  ## [0.17.2] - 2026-02-10
11
50
 
12
51
  ### Changed
@@ -293,6 +332,9 @@ This version was prepared but never released. Changes rolled into 0.17.2.
293
332
  - `cf_list_category` - List items by category
294
333
  - `cf_get_details` - Get full documentation by name
295
334
 
335
+ [Unreleased]: https://github.com/pusewicz/cf-mcp/compare/v0.20.0...HEAD
336
+ [0.20.0]: https://github.com/pusewicz/cf-mcp/compare/v0.19.0...v0.20.0
337
+ [0.19.0]: https://github.com/pusewicz/cf-mcp/compare/v0.18.0...v0.19.0
296
338
  [0.17.2]: https://github.com/pusewicz/cf-mcp/compare/v0.17.0...v0.17.2
297
339
  [0.17.1]: https://github.com/pusewicz/cf-mcp/compare/v0.17.0...v0.17.1
298
340
  [0.17.0]: https://github.com/pusewicz/cf-mcp/compare/v0.16.2...v0.17.0
data/Manifest.txt CHANGED
@@ -6,7 +6,6 @@ Rakefile
6
6
  config.ru
7
7
  config/puma.rb
8
8
  exe/cf-mcp
9
- exe/cf-mcp-wrapper
10
9
  lib/cf/mcp.rb
11
10
  lib/cf/mcp/cli.rb
12
11
  lib/cf/mcp/downloader.rb
data/README.md CHANGED
@@ -63,6 +63,12 @@ Then launch the inspector and connect using Streamable HTTP transport to:
63
63
  http://localhost:9292/http
64
64
  ```
65
65
 
66
+ When serving under a non-loopback hostname, allow it via `CF_MCP_ALLOWED_HOSTS` (comma-separated), otherwise requests are rejected with `403 Invalid Host header`:
67
+
68
+ ```bash
69
+ CF_MCP_ALLOWED_HOSTS=mcp.example.com cf-mcp http --root /path/to/cute_framework
70
+ ```
71
+
66
72
  ## Development
67
73
 
68
74
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
data/Rakefile CHANGED
@@ -9,7 +9,7 @@ require "standard/rake"
9
9
 
10
10
  desc "Generate Manifest.txt from git ls-files"
11
11
  task :manifest do
12
- ignore_patterns = %w[bin/ Gemfile .gitignore test/ .github/ .standard.yml cf-mcp.gemspec .ruby-version CLAUDE.md AGENTS.md fly.toml Procfile Dockerfile .dockerignore .claude/ .mcp.json]
12
+ ignore_patterns = %w[bin/ Gemfile .gitignore test/ .github/ .standard.yml cf-mcp.gemspec .ruby-version CLAUDE.md AGENTS.md fly.toml Procfile Dockerfile .dockerignore .claude/ .mcp.json .serena/]
13
13
 
14
14
  files = IO.popen(%w[git ls-files -z], chdir: __dir__, err: IO::NULL) do |ls|
15
15
  ls.readlines("\x0", chomp: true)
data/lib/cf/mcp/cli.rb CHANGED
@@ -97,8 +97,9 @@ module CF
97
97
  warn "Indexed #{count} topics from: #{path}" if event == :topics_indexed
98
98
  end
99
99
  warn "Indexed #{index.stats[:total]} items (#{index.stats[:functions]} functions, #{index.stats[:structs]} structs, #{index.stats[:enums]} enums)"
100
+ warn "Cute Framework revision: #{builder.revision}" if builder.revision
100
101
 
101
- server = Server.new(index)
102
+ server = Server.new(index, revision: builder.revision)
102
103
  server.run_stdio
103
104
  end
104
105
 
@@ -15,6 +15,8 @@ module CF
15
15
 
16
16
  class DownloadError < StandardError; end
17
17
 
18
+ attr_reader :sha
19
+
18
20
  def initialize(download_dir: DEFAULT_DOWNLOAD_DIR)
19
21
  @download_dir = download_dir
20
22
  end
@@ -30,6 +32,7 @@ module CF
30
32
  # Check if cache is valid
31
33
  stored_sha = read_sha_metadata(sha_file)
32
34
  latest_sha = fetch_latest_sha
35
+ @sha = latest_sha
33
36
 
34
37
  if stored_sha && latest_sha && stored_sha == latest_sha
35
38
  if File.directory?(include_path) && !Dir.empty?(include_path)
@@ -1,11 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "open3"
4
+
3
5
  module CF
4
6
  module MCP
5
7
  class IndexBuilder
6
8
  DEFAULT_HEADERS_PATH = File.expand_path("~/Work/GitHub/pusewicz/cute_framework/include")
7
9
 
8
- attr_reader :headers_path
10
+ attr_reader :headers_path, :revision
9
11
 
10
12
  def initialize(root: nil, download: false)
11
13
  @headers_path = resolve_headers_path(root: root, download: download)
@@ -41,20 +43,45 @@ module CF
41
43
  private
42
44
 
43
45
  def resolve_headers_path(root:, download:)
44
- return root if root
45
- return ENV["CF_HEADERS_PATH"] if ENV["CF_HEADERS_PATH"]
46
+ if root
47
+ @revision = detect_git_revision(root)
48
+ nested_include = File.join(root, "include")
49
+ return File.directory?(nested_include) ? nested_include : root
50
+ end
51
+
52
+ if (env_path = ENV["CF_HEADERS_PATH"])
53
+ @revision = detect_git_revision(env_path)
54
+ return env_path
55
+ end
46
56
 
47
57
  if download
48
58
  warn "Downloading Cute Framework headers from GitHub..."
49
59
  downloader = Downloader.new
50
60
  path = downloader.download_and_extract
61
+ @revision = downloader.sha
51
62
  warn "Downloaded headers to: #{path}"
52
63
  return path
53
64
  end
54
65
 
66
+ @revision = detect_git_revision(DEFAULT_HEADERS_PATH)
55
67
  DEFAULT_HEADERS_PATH
56
68
  end
57
69
 
70
+ # Detects the current git commit SHA for a local Cute Framework checkout.
71
+ # `path` may be the repo root or any subdirectory within it (e.g. include/) -
72
+ # git walks up to find the enclosing repository either way.
73
+ def detect_git_revision(path)
74
+ return nil unless File.directory?(path)
75
+
76
+ stdout, _stderr, status = Open3.capture3("git", "-C", path, "rev-parse", "--short", "HEAD")
77
+ return nil unless status.success?
78
+
79
+ sha = stdout.strip
80
+ sha.empty? ? nil : sha
81
+ rescue Errno::ENOENT
82
+ nil
83
+ end
84
+
58
85
  def find_topics_path(headers_path)
59
86
  # If headers_path is .../cute_framework/include, topics is at .../cute_framework/docs/topics
60
87
  base = File.dirname(headers_path)
data/lib/cf/mcp/parser.rb CHANGED
@@ -32,6 +32,7 @@ module CF
32
32
  def parse_directory(path)
33
33
  items = []
34
34
  Dir.glob(File.join(path, "**/*.h")).each do |header_file|
35
+ warn "Parsing #{header_file}" if $stderr.isatty
35
36
  items.concat(parse_file(header_file))
36
37
  end
37
38
  items
data/lib/cf/mcp/server.rb CHANGED
@@ -5,12 +5,14 @@ require "mcp"
5
5
  module CF
6
6
  module MCP
7
7
  class Server
8
- attr_reader :server, :index
8
+ attr_reader :server, :index, :revision
9
+
10
+ CUTE_FRAMEWORK_COMMIT_URL_TEMPLATE = "https://github.com/RandyGaul/cute_framework/commit/%{revision}"
9
11
 
10
12
  CORS_HEADERS = {
11
13
  "access-control-allow-origin" => "*",
12
14
  "access-control-allow-methods" => "GET, POST, DELETE, OPTIONS",
13
- "access-control-allow-headers" => "Content-Type, Accept, Mcp-Session-Id",
15
+ "access-control-allow-headers" => "Content-Type, Accept, MCP-Protocol-Version, Mcp-Method, Mcp-Name, Mcp-Session-Id",
14
16
  "access-control-expose-headers" => "Mcp-Session-Id"
15
17
  }.freeze
16
18
 
@@ -30,21 +32,32 @@ module CF
30
32
  warn "Indexed #{count} topics from: #{path}" if event == :topics_indexed
31
33
  end
32
34
  warn "Indexed #{index.stats[:total]} items (#{index.stats[:functions]} functions, #{index.stats[:structs]} structs, #{index.stats[:enums]} enums)"
35
+ warn "Cute Framework revision: #{builder.revision}" if builder.revision
33
36
 
34
- new(index).rack_app
37
+ new(index, revision: builder.revision).rack_app
35
38
  end
36
39
 
37
- PROTOCOL_VERSION = "2025-03-26"
40
+ # Modern clients carry their version on every request (no handshake); legacy
41
+ # clients negotiate through `initialize`. Both track the SDK rather than pinning.
42
+ MODERN_PROTOCOL_VERSION = ::MCP::Configuration::LATEST_MODERN_PROTOCOL_VERSION
43
+ LEGACY_PROTOCOL_VERSION = ::MCP::Configuration::LATEST_HANDSHAKE_PROTOCOL_VERSION
38
44
  WEBSITE_URL = ENV.fetch("FLY_APP_NAME", nil) ? "https://#{ENV["FLY_APP_NAME"]}.fly.dev" : "https://cf-mcp.fly.dev"
39
45
  PUBLIC_DIR = File.join(__dir__, "public")
46
+ ALLOWED_HOSTS_ENV = "CF_MCP_ALLOWED_HOSTS"
47
+
48
+ # Comma-separated list of extra Host header values the HTTP transport accepts
49
+ # (the MCP SDK only allows loopback hosts by default).
50
+ def self.allowed_hosts_from_env(env = ENV)
51
+ env.fetch(ALLOWED_HOSTS_ENV, "").split(",").map(&:strip).reject(&:empty?)
52
+ end
40
53
 
41
- def initialize(index)
54
+ def initialize(index, revision: nil, allowed_hosts: self.class.allowed_hosts_from_env)
42
55
  @index = index
56
+ @revision = revision
57
+ @allowed_hosts = allowed_hosts
43
58
 
44
- configuration = ::MCP::Configuration.new(protocol_version: PROTOCOL_VERSION)
45
59
  @server = ::MCP::Server.new(
46
60
  name: "cf-mcp",
47
- configuration:,
48
61
  version: CF::MCP::VERSION,
49
62
  icons: [
50
63
  ::MCP::Icon.new(src: "#{WEBSITE_URL}/favicon.svg", mime_type: "image/svg+xml", sizes: ["any"]),
@@ -78,7 +91,7 @@ module CF
78
91
  def rack_app
79
92
  require "rack"
80
93
 
81
- http_transport = ::MCP::Server::Transports::StreamableHTTPTransport.new(@server, stateless: true)
94
+ http_transport = ::MCP::Server::Transports::StreamableHTTPTransport.new(@server, stateless: true, allowed_hosts: @allowed_hosts)
82
95
  @server.transport = http_transport
83
96
 
84
97
  landing_page = build_landing_page
@@ -189,7 +202,10 @@ module CF
189
202
  ->(index, tool_classes) {
190
203
  context = TemplateContext.new(
191
204
  version: CF::MCP::VERSION,
192
- protocol_version: PROTOCOL_VERSION,
205
+ modern_protocol_version: MODERN_PROTOCOL_VERSION,
206
+ legacy_protocol_version: LEGACY_PROTOCOL_VERSION,
207
+ revision: @revision,
208
+ revision_url: @revision && format(CUTE_FRAMEWORK_COMMIT_URL_TEMPLATE, revision: @revision),
193
209
  stats: index.stats,
194
210
  categories: index.categories.sort,
195
211
  topics: index.topics_ordered.map { |t| {name: t.name, brief: t.brief} },
@@ -213,11 +229,14 @@ module CF
213
229
  class TemplateContext
214
230
  TEMPLATES_DIR = File.join(__dir__, "templates")
215
231
 
216
- attr_reader :version, :protocol_version, :stats, :categories, :topics, :tools, :tool_schemas_json
232
+ attr_reader :version, :modern_protocol_version, :legacy_protocol_version, :revision, :revision_url, :stats, :categories, :topics, :tools, :tool_schemas_json
217
233
 
218
- def initialize(version:, protocol_version:, stats:, categories:, topics:, tools:, tool_schemas_json:)
234
+ def initialize(version:, modern_protocol_version:, legacy_protocol_version:, revision:, revision_url:, stats:, categories:, topics:, tools:, tool_schemas_json:)
219
235
  @version = version
220
- @protocol_version = protocol_version
236
+ @modern_protocol_version = modern_protocol_version
237
+ @legacy_protocol_version = legacy_protocol_version
238
+ @revision = revision
239
+ @revision_url = revision_url
221
240
  @stats = stats
222
241
  @categories = categories
223
242
  @topics = topics
@@ -248,7 +267,10 @@ module CF
248
267
 
249
268
  def js_content
250
269
  js = File.read(File.join(TEMPLATES_DIR, "script.js"))
251
- js.sub("TOOL_SCHEMAS_PLACEHOLDER", @tool_schemas_json)
270
+ # Substitute the scalar placeholders first so injected content can't collide with them
271
+ js.sub("PROTOCOL_VERSION_PLACEHOLDER", @modern_protocol_version.to_json)
272
+ .sub("CLIENT_VERSION_PLACEHOLDER", @version.to_json)
273
+ .sub("TOOL_SCHEMAS_PLACEHOLDER", @tool_schemas_json)
252
274
  .sub("CATEGORIES_PLACEHOLDER", categories_json)
253
275
  .sub("TOPICS_PLACEHOLDER", topics_json)
254
276
  .sub("CHANGELOG_PLACEHOLDER", changelog_json)
@@ -53,8 +53,14 @@
53
53
  </tr>
54
54
  <tr>
55
55
  <th>Protocol Version</th>
56
- <td><%= protocol_version %></td>
56
+ <td><%= modern_protocol_version %> <small>(legacy handshake up to <%= legacy_protocol_version %>)</small></td>
57
57
  </tr>
58
+ <% if revision %>
59
+ <tr>
60
+ <th>Cute Framework Revision</th>
61
+ <td><a href="<%= h(revision_url) %>"><%= h(revision) %></a></td>
62
+ </tr>
63
+ <% end %>
58
64
  </table>
59
65
 
60
66
  <h2>Endpoints</h2>
@@ -8,6 +8,8 @@ const TOOLS = TOOL_SCHEMAS_PLACEHOLDER;
8
8
  const CATEGORIES = CATEGORIES_PLACEHOLDER;
9
9
  const TOPICS = TOPICS_PLACEHOLDER;
10
10
  const CHANGELOG = CHANGELOG_PLACEHOLDER;
11
+ const PROTOCOL_VERSION = PROTOCOL_VERSION_PLACEHOLDER;
12
+ const CLIENT_VERSION = CLIENT_VERSION_PLACEHOLDER;
11
13
 
12
14
  // Helper to render topic options (used in multiple places)
13
15
  const renderTopicOptions = () =>
@@ -38,13 +40,24 @@ function useMcpToolCall() {
38
40
  method: 'POST',
39
41
  headers: {
40
42
  'Content-Type': 'application/json',
41
- 'Accept': 'application/json, text/event-stream'
43
+ 'Accept': 'application/json, text/event-stream',
44
+ 'MCP-Protocol-Version': PROTOCOL_VERSION,
45
+ 'Mcp-Method': 'tools/call',
46
+ 'Mcp-Name': toolName
42
47
  },
43
48
  body: JSON.stringify({
44
49
  jsonrpc: '2.0',
45
50
  id: Date.now(),
46
51
  method: 'tools/call',
47
- params: { name: toolName, arguments: args }
52
+ params: {
53
+ name: toolName,
54
+ arguments: args,
55
+ _meta: {
56
+ 'io.modelcontextprotocol/protocolVersion': PROTOCOL_VERSION,
57
+ 'io.modelcontextprotocol/clientCapabilities': {},
58
+ 'io.modelcontextprotocol/clientInfo': { name: 'cf-mcp-dashboard', version: CLIENT_VERSION }
59
+ }
60
+ }
48
61
  })
49
62
  });
50
63
 
@@ -2,6 +2,6 @@
2
2
 
3
3
  module CF
4
4
  module MCP
5
- VERSION = "0.18.0"
5
+ VERSION = "0.20.0"
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cf-mcp
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.18.0
4
+ version: 0.20.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Piotr Usewicz
@@ -29,28 +29,28 @@ dependencies:
29
29
  requirements:
30
30
  - - "~>"
31
31
  - !ruby/object:Gem::Version
32
- version: '0.6'
32
+ version: '1.5'
33
33
  type: :runtime
34
34
  prerelease: false
35
35
  version_requirements: !ruby/object:Gem::Requirement
36
36
  requirements:
37
37
  - - "~>"
38
38
  - !ruby/object:Gem::Version
39
- version: '0.6'
39
+ version: '1.5'
40
40
  - !ruby/object:Gem::Dependency
41
41
  name: puma
42
42
  requirement: !ruby/object:Gem::Requirement
43
43
  requirements:
44
44
  - - "~>"
45
45
  - !ruby/object:Gem::Version
46
- version: '7.1'
46
+ version: '8.0'
47
47
  type: :runtime
48
48
  prerelease: false
49
49
  version_requirements: !ruby/object:Gem::Requirement
50
50
  requirements:
51
51
  - - "~>"
52
52
  - !ruby/object:Gem::Version
53
- version: '7.1'
53
+ version: '8.0'
54
54
  - !ruby/object:Gem::Dependency
55
55
  name: rack
56
56
  requirement: !ruby/object:Gem::Requirement
@@ -99,7 +99,6 @@ email:
99
99
  - piotr@layer22.com
100
100
  executables:
101
101
  - cf-mcp
102
- - cf-mcp-wrapper
103
102
  extensions: []
104
103
  extra_rdoc_files: []
105
104
  files:
@@ -111,7 +110,6 @@ files:
111
110
  - config.ru
112
111
  - config/puma.rb
113
112
  - exe/cf-mcp
114
- - exe/cf-mcp-wrapper
115
113
  - lib/cf/mcp.rb
116
114
  - lib/cf/mcp/cli.rb
117
115
  - lib/cf/mcp/downloader.rb
@@ -163,14 +161,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
163
161
  requirements:
164
162
  - - ">="
165
163
  - !ruby/object:Gem::Version
166
- version: 3.2.0
164
+ version: 3.3.0
167
165
  required_rubygems_version: !ruby/object:Gem::Requirement
168
166
  requirements:
169
167
  - - ">="
170
168
  - !ruby/object:Gem::Version
171
169
  version: '0'
172
170
  requirements: []
173
- rubygems_version: 4.0.3
171
+ rubygems_version: 4.0.20
174
172
  specification_version: 4
175
173
  summary: MCP server providing documentation tools for Cute Framework
176
174
  test_files: []
data/exe/cf-mcp-wrapper DELETED
@@ -1,8 +0,0 @@
1
- #!/bin/bash
2
- source /opt/homebrew/share/chruby/chruby.sh
3
-
4
- PROJECT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
5
- chruby "$(cat "$PROJECT_DIR/.ruby-version")"
6
-
7
- export BUNDLE_GEMFILE="$PROJECT_DIR/Gemfile"
8
- exec "$PROJECT_DIR/exe/cf-mcp" "$@"