Skip to content

Commit a97aff3

Browse files
committed
Per sha caching of CF docs
1 parent 34a6f08 commit a97aff3

File tree

13 files changed

+221
-40
lines changed

13 files changed

+221
-40
lines changed

CHANGELOG.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,19 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [Unreleased]
9+
10+
### Added
11+
12+
- SHA-based caching for downloaded Cute Framework headers - checks GitHub API for latest commit SHA before downloading to avoid redundant fetches
13+
- GitHub API client with optional GITHUB_TOKEN support for higher rate limits (5000/hr vs 60/hr unauthenticated)
14+
- Metadata tracking (.cf-mcp-sha file) stores downloaded version SHA for cache validation
15+
16+
### Changed
17+
18+
- Downloader uses commit-specific archive URLs (e.g., /archive/abc1234.zip) instead of always downloading master branch
19+
- Download process now checks for updates by comparing stored SHA with latest GitHub commit
20+
821
## [0.16.2] - 2026-01-31
922

1023
### Fixed

lib/cf/mcp.rb

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,25 +2,32 @@
22

33
require "pathname"
44
require_relative "mcp/version"
5-
require_relative "mcp/models/doc_item"
6-
require_relative "mcp/models/function_doc"
7-
require_relative "mcp/models/struct_doc"
8-
require_relative "mcp/models/enum_doc"
9-
require_relative "mcp/parser"
10-
require_relative "mcp/index"
11-
require_relative "mcp/index_builder"
12-
require_relative "mcp/server"
13-
require_relative "mcp/downloader"
14-
require_relative "mcp/cli"
155

166
module CF
177
module MCP
188
class Error < StandardError; end
199

10+
autoload :Parser, "cf/mcp/parser"
11+
autoload :Index, "cf/mcp/index"
12+
autoload :IndexBuilder, "cf/mcp/index_builder"
13+
autoload :TopicParser, "cf/mcp/topic_parser"
14+
autoload :Server, "cf/mcp/server"
15+
autoload :Downloader, "cf/mcp/downloader"
16+
autoload :GitHubClient, "cf/mcp/github_client"
17+
autoload :CLI, "cf/mcp/cli"
18+
2019
def self.root
2120
@root ||= Pathname.new(File.expand_path("../..", __dir__))
2221
end
2322

23+
module Models
24+
autoload :DocItem, "cf/mcp/models/doc_item"
25+
autoload :FunctionDoc, "cf/mcp/models/function_doc"
26+
autoload :StructDoc, "cf/mcp/models/struct_doc"
27+
autoload :EnumDoc, "cf/mcp/models/enum_doc"
28+
autoload :TopicDoc, "cf/mcp/models/topic_doc"
29+
end
30+
2431
module Tools
2532
autoload :SearchTool, "cf/mcp/tools/search_tool"
2633
autoload :ListCategory, "cf/mcp/tools/list_category"

lib/cf/mcp/cli.rb

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
# frozen_string_literal: true
22

33
require "optparse"
4-
require_relative "index_builder"
5-
require_relative "server"
64

75
module CF
86
module MCP

lib/cf/mcp/downloader.rb

Lines changed: 53 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,13 @@
44
require "uri"
55
require "fileutils"
66
require "zip"
7-
require_relative "version"
87

98
module CF
109
module MCP
1110
class Downloader
1211
CUTE_FRAMEWORK_ZIP_URL = "https://github.com/RandyGaul/cute_framework/archive/refs/heads/master.zip"
12+
GITHUB_ARCHIVE_URL_TEMPLATE = "https://github.com/RandyGaul/cute_framework/archive/%{ref}.zip"
13+
SHA_METADATA_FILE = ".cf-mcp-sha"
1314
DEFAULT_DOWNLOAD_DIR = File.join(Dir.tmpdir, "cf-mcp-#{VERSION}")
1415

1516
class DownloadError < StandardError; end
@@ -24,23 +25,41 @@ def download_and_extract
2425
zip_path = File.join(@download_dir, "cute_framework.zip")
2526
base_path = File.join(@download_dir, "cute_framework")
2627
include_path = File.join(base_path, "include")
27-
File.join(base_path, "docs", "topics")
28+
sha_file = File.join(@download_dir, SHA_METADATA_FILE)
2829

29-
# Return existing path if already downloaded
30-
if File.directory?(include_path) && !Dir.empty?(include_path)
31-
return include_path
30+
# Check if cache is valid
31+
stored_sha = read_sha_metadata(sha_file)
32+
latest_sha = fetch_latest_sha
33+
34+
if stored_sha && latest_sha && stored_sha == latest_sha
35+
if File.directory?(include_path) && !Dir.empty?(include_path)
36+
warn "Using cached Cute Framework headers (SHA: #{stored_sha})"
37+
return include_path
38+
end
39+
end
40+
41+
# Determine download URL
42+
if latest_sha
43+
download_url = format(GITHUB_ARCHIVE_URL_TEMPLATE, ref: latest_sha)
44+
warn "Downloading Cute Framework at SHA #{latest_sha}..."
45+
else
46+
download_url = CUTE_FRAMEWORK_ZIP_URL
47+
warn "Downloading Cute Framework from master branch..."
3248
end
3349

34-
download_zip(zip_path)
50+
download_zip(zip_path, download_url)
3551
extract_directories(zip_path, base_path)
3652

53+
# Store metadata for future cache checks
54+
write_sha_metadata(sha_file, latest_sha) if latest_sha
55+
3756
include_path
3857
end
3958

4059
private
4160

42-
def download_zip(destination)
43-
uri = URI.parse(CUTE_FRAMEWORK_ZIP_URL)
61+
def download_zip(destination, url = CUTE_FRAMEWORK_ZIP_URL)
62+
uri = URI.parse(url)
4463

4564
Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
4665
request = Net::HTTP::Get.new(uri)
@@ -73,9 +92,9 @@ def extract_directories(zip_path, base_path)
7392
top_level_prefix = nil
7493

7594
zip_file.each do |entry|
76-
# Find the top-level directory prefix (e.g., "cute_framework-master/")
77-
if top_level_prefix.nil? && entry.name.match?(%r{^[^/]+/include/})
78-
top_level_prefix = entry.name.match(%r{^([^/]+/)})[1]
95+
# Find the top-level directory prefix (e.g., "cute_framework-master/" or "cute_framework-abc1234/")
96+
if top_level_prefix.nil? && entry.name.match?(%r{^cute_framework-[^/]+/include/})
97+
top_level_prefix = entry.name.match(%r{^(cute_framework-[^/]+/)})[1]
7998
break
8099
end
81100
end
@@ -107,6 +126,29 @@ def extract_directories(zip_path, base_path)
107126
end
108127
end
109128
end
129+
130+
def fetch_latest_sha
131+
client = GitHubClient.new
132+
client.latest_commit_sha
133+
rescue => e
134+
warn "GitHub API error: #{e.message}"
135+
nil
136+
end
137+
138+
def read_sha_metadata(file)
139+
return nil unless File.exist?(file)
140+
File.read(file).strip
141+
rescue => e
142+
warn "Error reading SHA metadata: #{e.message}"
143+
nil
144+
end
145+
146+
def write_sha_metadata(file, sha)
147+
return unless sha
148+
File.write(file, sha)
149+
rescue => e
150+
warn "Error writing SHA metadata: #{e.message}"
151+
end
110152
end
111153
end
112154
end

lib/cf/mcp/github_client.rb

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# frozen_string_literal: true
2+
3+
require "net/http"
4+
require "json"
5+
require "uri"
6+
7+
module CF
8+
module MCP
9+
class GitHubClient
10+
GITHUB_API_BASE = "https://api.github.com"
11+
REPO_OWNER = "RandyGaul"
12+
REPO_NAME = "cute_framework"
13+
DEFAULT_BRANCH = "master"
14+
15+
def initialize(token: ENV["GITHUB_TOKEN"])
16+
@token = token
17+
end
18+
19+
# Returns latest commit SHA (short 7-char format) or nil on failure
20+
def latest_commit_sha
21+
uri = URI.parse("#{GITHUB_API_BASE}/repos/#{REPO_OWNER}/#{REPO_NAME}/commits/#{DEFAULT_BRANCH}")
22+
23+
request = Net::HTTP::Get.new(uri)
24+
request["Accept"] = "application/vnd.github+json"
25+
request["Authorization"] = "Bearer #{@token}" if @token
26+
request["User-Agent"] = "cf-mcp/#{CF::MCP::VERSION}"
27+
28+
response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
29+
http.request(request)
30+
end
31+
32+
return nil unless response.is_a?(Net::HTTPSuccess)
33+
34+
data = JSON.parse(response.body)
35+
data.dig("sha")&.slice(0, 7) # Return short SHA
36+
rescue
37+
# Return nil on any error (network, JSON parse, etc.)
38+
nil
39+
end
40+
end
41+
end
42+
end

lib/cf/mcp/index_builder.rb

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,5 @@
11
# frozen_string_literal: true
22

3-
require_relative "parser"
4-
require_relative "topic_parser"
5-
require_relative "index"
6-
require_relative "downloader"
7-
83
module CF
94
module MCP
105
class IndexBuilder

lib/cf/mcp/models/enum_doc.rb

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
# frozen_string_literal: true
22

3-
require_relative "doc_item"
4-
53
module CF
64
module MCP
75
module Models

lib/cf/mcp/models/function_doc.rb

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
# frozen_string_literal: true
22

3-
require_relative "doc_item"
4-
53
module CF
64
module MCP
75
module Models

lib/cf/mcp/models/struct_doc.rb

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
# frozen_string_literal: true
22

3-
require_relative "doc_item"
4-
53
module CF
64
module MCP
75
module Models

lib/cf/mcp/parser.rb

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,5 @@
11
# frozen_string_literal: true
22

3-
require_relative "models/function_doc"
4-
require_relative "models/struct_doc"
5-
require_relative "models/enum_doc"
6-
73
module CF
84
module MCP
95
class Parser

0 commit comments

Comments
 (0)