Skip to content

build(deps): bump kamal from 2.5.2 to 2.7.0 #15

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
// Mount host's ~/.claude if it exists
"mounts": [
"source=${localEnv:HOME}/.claude,target=/home/vscode/.claude,type=bind,consistency=cached",
"source=${localEnv:HOME}/.claude.json,target=/home/vscode/.claude.json,type=bind,consistency=cached"
"source=${localEnv:HOME}/.claude.json,target=/home/vscode/.claude.json,type=bind,consistency=cached",
"source=${localEnv:HOME}/.mcp.json,target=/home/vscode/.mcp.json,type=bind,consistency=cached"
]
}
5 changes: 5 additions & 0 deletions .mcp.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"mcpServers": {
"spotlight-rails": {}
}
}
3 changes: 3 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -75,3 +75,6 @@ gem "metainspector", "~> 5.15"
gem "rswag-api"
gem "rswag-ui"
gem "rswag-specs", group: [ :development, :test ]

# Model Context Protocol (MCP) for LLM integration
gem "mcp"
32 changes: 18 additions & 14 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -75,28 +75,28 @@ GEM
addressable (2.8.7)
public_suffix (>= 2.0.2, < 7.0)
ast (2.4.2)
base64 (0.2.0)
base64 (0.3.0)
bcrypt_pbkdf (1.1.1)
benchmark (0.4.0)
bigdecimal (3.1.9)
benchmark (0.4.1)
bigdecimal (3.2.2)
bindex (0.8.1)
bootsnap (1.18.4)
msgpack (~> 1.2)
brakeman (7.0.2)
racc
builder (3.3.0)
concurrent-ruby (1.3.5)
connection_pool (2.5.0)
connection_pool (2.5.3)
crass (1.0.6)
date (3.4.1)
debug (1.10.0)
irb (~> 1.10)
reline (>= 0.3.8)
diff-lcs (1.6.1)
domain_name (0.6.20240107)
dotenv (3.1.7)
drb (2.2.1)
ed25519 (1.3.0)
dotenv (3.1.8)
drb (2.2.3)
ed25519 (1.4.0)
erubi (1.13.1)
et-orbi (1.2.11)
tzinfo
Expand Down Expand Up @@ -163,20 +163,21 @@ GEM
json-schema (5.1.1)
addressable (~> 2.8)
bigdecimal (~> 3.1)
kamal (2.5.2)
json_rpc_handler (0.1.1)
kamal (2.7.0)
activesupport (>= 7.0)
base64 (~> 0.2)
bcrypt_pbkdf (~> 1.0)
concurrent-ruby (~> 1.2)
dotenv (~> 3.1)
ed25519 (~> 1.2)
ed25519 (~> 1.4)
net-ssh (~> 7.3)
sshkit (>= 1.23.0, < 2.0)
thor (~> 1.3)
zeitwerk (>= 2.6.18, < 3.0)
language_server-protocol (3.17.0.4)
lint_roller (1.1.0)
logger (1.6.6)
logger (1.7.0)
loofah (2.24.0)
crass (~> 1.0.2)
nokogiri (>= 1.12.0)
Expand All @@ -186,6 +187,8 @@ GEM
net-pop
net-smtp
marcel (1.0.4)
mcp (0.1.0)
json_rpc_handler (~> 0.1)
metainspector (5.15.0)
addressable (~> 2.8.4)
faraday (~> 2.5)
Expand All @@ -203,7 +206,7 @@ GEM
benchmark
logger
mini_mime (1.1.5)
minitest (5.25.4)
minitest (5.25.5)
msgpack (1.8.0)
nesty (1.0.2)
net-http (0.6.0)
Expand Down Expand Up @@ -235,7 +238,7 @@ GEM
racc (~> 1.4)
nokogiri (1.18.3-x86_64-linux-musl)
racc (~> 1.4)
ostruct (0.6.1)
ostruct (0.6.2)
parallel (1.26.3)
parser (3.3.7.1)
ast (~> 2.4.1)
Expand Down Expand Up @@ -419,7 +422,7 @@ GEM
unicode-display_width (3.1.4)
unicode-emoji (~> 4.0, >= 4.0.4)
unicode-emoji (4.0.4)
uri (1.0.2)
uri (1.0.3)
useragent (0.16.11)
view_component (3.21.0)
activesupport (>= 5.2.0, < 8.1)
Expand All @@ -434,7 +437,7 @@ GEM
base64
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.5)
zeitwerk (2.7.2)
zeitwerk (2.7.3)
zlib (2.1.1)

PLATFORMS
Expand All @@ -457,6 +460,7 @@ DEPENDENCIES
importmap-rails
jbuilder
kamal
mcp
metainspector (~> 5.15)
propshaft
puma (>= 5.0)
Expand Down
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,21 @@ bundle exec rails rswag:specs:swaggerize
```

http://localhost:3000/api-docs/

## MCP サーバー
記事の更新のためのMCPサーバーを実装しています。

```json:~/.mcp.json
{
"mcpServers": {
"spotlight-rails": {
"type": "http",
"url": "https://takeyuweb.co.jp/api/mcp",
"method": "POST",
"headers": {
"Authorization": "Bearer token"
}
}
}
}
```
51 changes: 51 additions & 0 deletions app/controllers/api/mcp_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# frozen_string_literal: true

module Api
class McpController < ApplicationController
skip_before_action :verify_authenticity_token
before_action :authenticate_mcp_request

def handle
# Create MCP server instance
server = ArticleServer.create

# Handle the JSON-RPC request
response = server.handle_json(request.raw_post)

# Return response
render json: response
rescue => e
Rails.logger.error "MCP Error: #{e.message}"
Rails.logger.error e.backtrace.join("\n")

render json: {
jsonrpc: "2.0",
error: {
code: -32603,
message: "Internal error",
data: { details: e.message }
},
id: nil
}, status: :internal_server_error
end

private

def authenticate_mcp_request
token = request.headers["Authorization"]&.gsub(/^Bearer\s+/, "")
expected_token = Rails.application.credentials.dig(:mcp, :api_token)

unless token.present? && ActiveSupport::SecurityUtils.secure_compare(token, expected_token)
render json: {
jsonrpc: "2.0",
error: {
code: -32603,
message: "Unauthorized",
data: { details: "Invalid or missing authentication token" }
},
id: params[:id]
}, status: :unauthorized
end
end
end
end
16 changes: 16 additions & 0 deletions app/mcp/article_server.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# frozen_string_literal: true

class ArticleServer
def self.create
MCP::Server.new(
name: "spotlight-rails-articles",
version: "1.0.0",
tools: [
Tools::CreateArticleTool,
Tools::UpdateArticleTool,
Tools::FindArticleTool
],
server_context: {}
)
end
end
42 changes: 42 additions & 0 deletions app/mcp/tools/create_article_tool.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# frozen_string_literal: true

module Tools
class CreateArticleTool < MCP::Tool
description "Create a new article from markdown content with YAML frontmatter"

input_schema(
properties: {
content: {
type: "string",
description: "The markdown content with YAML frontmatter (including title, slug, description, published_date, tags, etc.)"
}
},
required: [ "content" ]
)

def self.call(content:, server_context:)
article = Article.import_from_markdown(content)

if article
MCP::Tool::Response.new([ {
type: "text",
text: "Article created successfully:\n" \
"- Title: #{article.title}\n" \
"- Slug: #{article.slug}\n" \
"- Published at: #{article.published_at}\n" \
"- Tags: #{article.tags.pluck(:name).join(', ')}"
} ])
else
MCP::Tool::Response.new([ {
type: "text",
text: "Failed to create article. Please check the markdown content and ensure it has valid YAML frontmatter with required fields (title, slug, description, published_date)."
} ])
end
rescue => e
MCP::Tool::Response.new([ {
type: "text",
text: "Error creating article: #{e.message}"
} ])
end
end
end
45 changes: 45 additions & 0 deletions app/mcp/tools/find_article_tool.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# frozen_string_literal: true

module Tools
class FindArticleTool < MCP::Tool
description "Find an article by slug and return its details"

input_schema(
properties: {
slug: {
type: "string",
description: "The slug of the article to find"
}
},
required: [ "slug" ]
)

def self.call(slug:, server_context:)
article = Article.find_by(slug: slug)

if article
MCP::Tool::Response.new([ {
type: "text",
text: "Article found:\n" \
"- Title: #{article.title}\n" \
"- Slug: #{article.slug}\n" \
"- Description: #{article.description}\n" \
"- Published at: #{article.published_at}\n" \
"- Tags: #{article.tags.pluck(:name).join(', ')}\n" \
"- Created at: #{article.created_at}\n" \
"- Updated at: #{article.updated_at}"
} ])
else
MCP::Tool::Response.new([ {
type: "text",
text: "Article not found with slug: #{slug}"
} ])
end
rescue => e
MCP::Tool::Response.new([ {
type: "text",
text: "Error finding article: #{e.message}"
} ])
end
end
end
57 changes: 57 additions & 0 deletions app/mcp/tools/update_article_tool.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# frozen_string_literal: true

module Tools
class UpdateArticleTool < MCP::Tool
description "Update an existing article by slug with new markdown content"

input_schema(
properties: {
slug: {
type: "string",
description: "The slug of the article to update"
},
content: {
type: "string",
description: "The new markdown content with YAML frontmatter"
}
},
required: [ "slug", "content" ]
)

def self.call(slug:, content:, server_context:)
# Find the existing article
article = Article.find_by(slug: slug)

unless article
return MCP::Tool::Response.new([ {
type: "text",
text: "Article not found with slug: #{slug}"
} ])
end

# Update the article with new content
updated_article = Article.import_from_markdown(content)

if updated_article
MCP::Tool::Response.new([ {
type: "text",
text: "Article updated successfully:\n" \
"- Title: #{updated_article.title}\n" \
"- Slug: #{updated_article.slug}\n" \
"- Published at: #{updated_article.published_at}\n" \
"- Tags: #{updated_article.tags.pluck(:name).join(', ')}"
} ])
else
MCP::Tool::Response.new([ {
type: "text",
text: "Failed to update article. Please check the markdown content and ensure it has valid YAML frontmatter."
} ])
end
rescue => e
MCP::Tool::Response.new([ {
type: "text",
text: "Error updating article: #{e.message}"
} ])
end
end
end
Loading
Loading