Skip to content

Commit a402adf

Browse files
committed
SchnellMCP post
1 parent 9ac294f commit a402adf

File tree

7 files changed

+442
-2
lines changed

7 files changed

+442
-2
lines changed
66.7 KB
Loading
168 KB
Loading

md/schnellmcp.md

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
*Do you have, like me, a bunch of Ruby scripts that make your life easier? Why not make them available to your [e-friends](https://en.wikipedia.org/wiki/AI_assistant) too?*
2+
3+
## The Trigger
4+
5+
A few weeks ago, I was exploring **[Temporal](https://temporal.io/)** (looks promising, worth checking out) and stumbled into a [tutorial](https://docs.temporal.io/ai-cookbook/hello-world-durable-mcp-server) about building an [**MCP server**](https://modelcontextprotocol.io/docs/learn/server-concepts). It used Python with the [**FastMCP library**](https://gofastmcp.com/getting-started/welcome). I'd been curious about **[MCP](https://modelcontextprotocol.io/docs/getting-started/intro)** for a while but kept putting it off, thinking it'd be complicated.
6+
7+
Then I saw the example. It was **suspiciously simple**: a plain Python method, a docstring, type annotations, and a single decorator. That's it. One decorator, and suddenly your e-friend can call it. That simplicity caught me. **Could Ruby feel this natural?**
8+
9+
<figure>
10+
<picture>
11+
<img
12+
src="/assets/img/writings/schnell_python.png"
13+
alt="This amazed me, are you amazed also?"
14+
>
15+
</picture>
16+
<figcaption>
17+
This amazed me, are you amazed also?
18+
</figcaption>
19+
</figure>
20+
21+
---
22+
23+
## What's Already Out There
24+
25+
I started looking. What did Ruby already have for MCP?
26+
27+
- [**ruby-sdk**](https://github.com/modelcontextprotocol/ruby-sdk) — maintained by <img src="https://avatars.githubusercontent.com/u/13203?v=4" class="avatar"> [Koichi ITO (@koic)](https://github.com/koic)
28+
- Official, low-level protocol implementation
29+
- Great foundation for building on top
30+
- [**mcp-rb**](https://github.com/funwarioisii/mcp-rb) — created by <img src="https://avatars.githubusercontent.com/u/20943066?v=4" class="avatar"> [Kazuyuki Hashimoto (@funwarioisii)](https://github.com/funwarioisii)
31+
- Full-featured, supports resources and templates
32+
- Rake/Thor-like experience with tool definitions separate from code
33+
- [**fast-mcp**](https://github.com/yjacquin/fast-mcp) — created by <img src="https://avatars.githubusercontent.com/u/51963063?v=4" class="avatar"> [Yorick Jacquin (@yjacquin)](https://github.com/yjacquin)
34+
- Chained DSL doesn't quite have that Ruby feel
35+
36+
All solid projects doing great work. But I wanted something different: **take code I've already written, add annotations, and it becomes an MCP server.** No DSL to learn. No separate tool definitions. Why write special instructions just for your e-friend when the code and its docs already say everything that needs saying?
37+
38+
## The Idea
39+
40+
Then it clicked. Ruby's had method documentation for decades — [**RDoc**](https://ruby.github.io/rdoc/) and [**YARD**](https://yardoc.org/). I already write `@param` types and `@return` descriptions. That metadata is already there.
41+
42+
What if one tag — just `@mcp.tool` — could turn a documented method into something your e-friend can call? No new syntax. No separation between "tool definition" and "actual code." Just annotated Ruby.
43+
44+
Bonus: LLMs love generating YARD comments. I usually delete them. **Finally, a reason to keep them** — turns out e-friends benefit from the same docs you do.
45+
46+
## Building It
47+
48+
The result is what I currently call [**SchnellMCP**](https://github.com/RubyElders/schnellmcp). Plain Ruby methods + YARD docs = (also) MCP tools. Here's a simple example:
49+
50+
```ruby
51+
require 'bundler/inline'
52+
53+
gemfile do
54+
source 'https://gem.coop'
55+
gem 'schnellmcp', git: 'https://github.com/RubyElders/schnellmcp'
56+
end
57+
58+
require 'schnellmcp'
59+
60+
# Add two numbers
61+
#
62+
# @param a [Integer] First number
63+
# @param b [Integer] Second number
64+
#
65+
# @return [Integer] Sum of a and b
66+
#
67+
# @mcp.tool
68+
def add(a, b)
69+
a + b
70+
end
71+
72+
# Start the MCP server if called directly
73+
SchnellMCP::Server.run(__FILE__) if __FILE__ == $0
74+
```
75+
76+
Standard YARD documentation. One `@mcp.tool` tag. Done. The MCP tool description and parameter types come straight from the docs. Type casting is automatic.
77+
78+
Code that feels like Ruby, not "MCP framework code." That's what I was after.
79+
80+
## Daily Use
81+
82+
I've been working on a C++ project — not my strongest language, so LLMs help a lot. The usual flow: write code, compile, test, repeat. But C++ build state is a minefield. What's the current state of the build? Is my latest change actually included? Did cache invalidation properly detect my file changed during compilation? Is the build folder properly bootstrapped and up to date with the latest CMake configuration? These questions sound silly until you've wasted an hour debugging code that wasn't even running.
83+
84+
Classic example: add a log line to debug something, run the code, line doesn't appear. Now what? Was the file rebuilt at all? Do I need to rebuild and try again? Clear the cache and start fresh just to be sure? Without knowing the history and current state, it's genuinely hard to tell. Sound familiar?
85+
86+
I wrapped the whole thing into a Ruby script — one command that ensures every run starts from a predictable state. Build folder, cache, compilation — handled, every time. No more build archaeology before getting to the actual problem. Sometimes I trigger it myself, sometimes it's part of a larger script. No more guessing, no more side-quests.
87+
88+
And then with one `@mcp.tool` annotation, the same script became available to my e-friend too. Same tool, same predictable state — now my e-friend doesn't get sidetracked on build archaeology either, and can focus on what actually matters.
89+
90+
One plain Ruby file covers all three use cases naturally: CLI for quick manual runs, MCP server for the e-friends, and a regular `require` when another script needs it. No extra framework, no duplication, just Ruby. Here's a partial example — just the preset listing task — to show the idea:
91+
92+
```ruby
93+
require 'bundler/inline'
94+
95+
gemfile do
96+
source 'https://gem.coop'
97+
gem 'schnellmcp', git: 'https://github.com/RubyElders/schnellmcp'
98+
end
99+
100+
require 'schnellmcp'
101+
require 'json'
102+
103+
# List available CMake presets
104+
#
105+
# @param path [String] Path to directory containing CMakePresets.json
106+
#
107+
# @return [Array<String>] List of available preset names
108+
#
109+
# @mcp.tool
110+
def list_cmake_presets(path = ".")
111+
presets_file = File.join(path, "CMakePresets.json")
112+
data = JSON.parse(File.read(presets_file))
113+
data["configurePresets"].map { |preset| preset["name"] }
114+
end
115+
116+
# Run based on how the script was invoked
117+
if __FILE__ == $0
118+
if ARGV[0] == "server"
119+
# MCP server mode for e-friends
120+
SchnellMCP::Server.run(__FILE__)
121+
else
122+
# CLI mode for humans
123+
puts list_cmake_presets(".")
124+
end
125+
end
126+
```
127+
128+
One file, three modes:
129+
130+
- `ruby cmake.rb server` — MCP server for my e-friend
131+
- `ruby cmake.rb` — CLI for quick checks
132+
- `require_relative 'cmake'` — library for other scripts
133+
134+
Same code, three ways to use it. Plain Ruby throughout — testable, composable, nothing special.
135+
136+
The CMake example captures the whole idea: I wrote the script because I needed clarity. My e-friend needed that same clarity. One tool, useful for everyone — including my e-friend.
137+
138+
## What's Next?
139+
140+
**SchnellMCP is a prototype** — single files only, no dependency parsing. Just enough to work with [GitHub Copilot](https://github.com/features/copilot), which is what I use. And I've been running it daily for months, steadily adding more tools as I find scripts worth exposing.
141+
142+
It would probably deserve a proper backend at some point — like the aforementioned [ruby-sdk](https://github.com/modelcontextprotocol/ruby-sdk) for solid protocol handling — but even with the current minimal and hacky code it still fully works, and honestly I haven't found a reason to rush that yet.
143+
144+
Check it out at [**github.com/RubyElders/schnellmcp**](https://github.com/RubyElders/schnellmcp) — and if you have a drawer full of little Ruby scripts, you're probably closer to an MCP server than you think.
145+
146+
**Have your own stories of Ruby tools and e-friend integration? Come say hi on** [Mastodon](https://ruby.social/@rubyelders), [Bluesky](https://rubyelders.bsky.social) **or** [Twitter/X](https://x.com/RubyElders) **— and above all, good luck educating your e-friends with your own dev habits. 🤖**

writings.atom

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,15 @@
66
</author>
77
<id>https://rubyelders.com/writings.html</id>
88
<title>Ruby Elders&#39; writings</title>
9-
<updated>2026-02-20T13:18:32+01:00</updated>
9+
<updated>2026-02-20T16:47:37+01:00</updated>
10+
<entry>
11+
<id>https://rubyelders.com/writings/2026-02-schnellmcp.html</id>
12+
<link href="https://rubyelders.com/writings/2026-02-schnellmcp.html"/>
13+
<summary>Inspired by Python&#39;s FastMCP simplicity, SchnellMCP brings the same elegant developer experience to Ruby. Create powerful MCP servers with plain Ruby methods, native type annotations, and a single decorator — no complexity, just Ruby doing what it does best.</summary>
14+
<title>SchnellMCP: Ruby native MCP server experience</title>
15+
<updated>2026-02-20T00:00:00+01:00</updated>
16+
<dc:date>2026-02-20T00:00:00+01:00</dc:date>
17+
</entry>
1018
<entry>
1119
<id>https://rubyelders.com/writings/2026-01-ruby-box-reload.html</id>
1220
<link href="https://rubyelders.com/writings/2026-01-ruby-box-reload.html"/>
@@ -38,5 +46,5 @@
3846
<updated>2025-08-12T00:00:00+02:00</updated>
3947
<dc:date>2025-08-12T00:00:00+02:00</dc:date>
4048
</entry>
41-
<dc:date>2026-02-20T13:18:32+01:00</dc:date>
49+
<dc:date>2026-02-20T16:47:37+01:00</dc:date>
4250
</feed>

writings.html

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,11 @@
7070
<h1>Latest writings</h1>
7171
<ul class="post-list">
7272

73+
<li>
74+
<a href="writings/2026-02-schnellmcp.html">SchnellMCP: Ruby native MCP server experience</a>
75+
<time datetime="2026-02-20">February 20, 2026</time>
76+
</li>
77+
7378
<li>
7479
<a href="writings/2026-01-ruby-box-reload.html">Ruby::Box: Rethinking Code Reloading with Isolated Namespaces</a>
7580
<time datetime="2026-01-25">January 25, 2026</time>

writings.json

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,19 @@
11
[
2+
{
3+
"source": "md/schnellmcp.md",
4+
"title": "SchnellMCP: Ruby native MCP server experience",
5+
"published_at": "20.2.2026",
6+
"slug": "2026-02-schnellmcp",
7+
"author": "simi",
8+
"author_avatar_url": "https://avatars.githubusercontent.com/u/193936?v=4",
9+
"author_url": "https://github.com/simi",
10+
"place": "Praha, Žižkov",
11+
"og": {
12+
"title": "SchnellMCP: Ruby native MCP server experience",
13+
"description": "Inspired by Python's FastMCP simplicity, SchnellMCP brings the same elegant developer experience to Ruby. Create powerful MCP servers with plain Ruby methods, native type annotations, and a single decorator — no complexity, just Ruby doing what it does best.",
14+
"image": "https://rubyelders.com/assets/img/writings/schnell_og_image.png"
15+
}
16+
},
217
{
318
"source": "md/ruby-box-reload.md",
419
"title": "Ruby::Box: Rethinking Code Reloading with Isolated Namespaces",

0 commit comments

Comments
 (0)