Sr. Content Developer at Microsoft, working remotely in PA, TechBash conference organizer, former Microsoft MVP, Husband, Dad and Geek.
159425 stories
·
33 followers

πŸ“± Four iOS apps, one year, and a lot of help from AI

1 Share

In September of 2025 I started tinkering building an iPhone app. Before this amazing AI wave I thought I would need to retire, learn Swift UI and spend the better part of a few months learning the basics of how to build an iOS app. Well it turns out I was wrong and I have now built 5 iOS apps that are published on the App Store.

It all started with an idea my sister Nevine and I discussed.

Thanks for reading OmarKnows! Subscribe for free to receive new posts and support my work.

My photo library has tens of thousands of photos in it, and every attempt to clean it up died the same way: I would open Photos, scroll for ten minutes, feel overwhelmed, and close it. What if instead of looking at everything, I only looked at the photos taken on this day across all the years? Today’s date, every year I have owned an iPhone. A small, finite pile. Something I could actually finish over coffee.

So I built it. And the surprising part was not the cleanup. It was that looking at one day across all those years connected me to my memories and my past in a way that scrolling never had. Kids at every age. Trips I had half forgotten. That turned it from a chore into something I looked forward to, and it’s why I kept going: widgets so the day’s memories are on my Home Screen, a morning notification with a photo from that day, and sharing so I could send those memories to family.

That first app was an early experiment in building with AI. I used Cursor and Claude Opus 4.5 and 4.6, and I learned a lot about what worked and what didn’t. At the start of 2026 I moved to Codex and Claude Code as my main tools, got faster and better at this, and the apps started coming out quicker. Somewhere along the way I started calling it Omar’s Software Factory.

Here is what came off the factory floor. All four are on the App Store, all four are built for iOS 26, and none of them have accounts, ads, or tracking. Details for each live at omarknows.app.

On This Day

On This Day is the app that started it all. Open it and you see every photo and video you took on today’s date, across every year. Swipe right to keep, swipe left to delete. Deletes are queued until you’re done, and at the end of a session it tells you how much space you actually got back.

The daily part is what makes it work. You don’t clean up your library. You clean up one day, and tomorrow there’s a new day waiting. There’s a streak, a calendar with a dot on every day you’ve finished, and a random day button for when today is thin.

The parts I added because I kept wanting them:

  • Widgets in three sizes that show today’s memory and roll over at midnight.

  • A morning notification with a photo from this day in a past year. It’s a nice way to start the day.

  • Sharing that includes the date and the place the photo was taken. Sending a “ten years ago today” photo to Lora or the kids is my favorite part.

  • Group by Year in the grid, so you can jump straight to 2014.

  • Live Photos and video play inline, EXIF metadata is a tap away, and you can add the whole day to an album.

Everything happens on your phone. Photos never leave the device, and the only thing that syncs through iCloud is your keep and delete decisions so you don’t see the same photo twice.

On This Day is free until you’ve reviewed seven days. After that it’s $1.99 a month, $19.99 a year, or $59.99 for life, which unlocks unlimited days, the widgets, and custom app icons. iPhone only for now.

Tallyday

I wanted a counter for the things in life that don’t have a natural log. Days since I got married. Days since I started at Microsoft. Days since I started Strength Training. So I built Tallyday.

Each counter is a card with a name, an icon, and a color. Tap it, hit Reset, and you’re back to day zero. Tap the number and it flips from total days to years, months, and days. It counts down too: set a date for a trip or a launch, watch the days tick away, and when the day arrives it celebrates and then quietly starts counting up.

There are widgets (one counter, two side by side, or a list), reminders on whatever cadence you like, categories, archive, and search. Your counters sync through your own iCloud, so they show up on your iPad without an account. You can share a milestone as a card, or send it as a link. And a fun detail: the link never touches a server. The milestone lives inside the URL itself, so nothing about your counter is logged anywhere.

Tallyday is free. There’s a tip jar if you want to say thanks, and tips unlock nothing but my gratitude. iPhone and iPad.

Share Times

Every week I have some version of this exchange: “Do you have time Thursday?” “Sure, what works?” “How about 2?” “Is that your 2 or my 2?”

Share Times is my answer. It shows your real calendar as a day timeline, you tap or drag on the empty spots to mark when you’re free, and it produces a clean block of text you can paste into Messages, Mail, or Slack.

The killer feature is time zones. Add the recipient’s time zone and Share Times converts every slot into their local time, so the message reads correctly on their end. No mental math. It handles 15, 30, or 60 minute blocks, working hours, and it skips weekends by default. Recipients can tap the times and see them in their own calendar.

There’s also a three-day calendar widget: a clean, glanceable view of the next few days that looks great with iOS 26.

Share Times is free for a single calendar. Pro is $4.99 a year or $19.99 for life and adds multiple calendars, recipient time zones, the widgets, and lets you remove the “sent with Share Times” line. iPhone only.

Netgleam

Netgleam is the newest one, and it came from travel frustration. Hotel Wi-Fi that connects but doesn’t work. Airport lounges where the captive portal never pops up. Airplane Wi-Fi that says it’s fine while nothing loads. I wanted one app that could tell me plainly what my connection was actually doing.

Netgleam does that. Open it and you get a status card: online or not, Wi-Fi or cellular, link quality, IPv4 and IPv6, whether you’re on a metered connection, whether traffic is going through a VPN or Private Relay. If it detects a captive portal, there’s a one-tap button to open it. I tested that one live on lounge Wi-Fi at JFK and on Alaska’s inflight Wi-Fi, and both were the kind of networks that fool Apple’s built-in detection.

The other tabs:

  • Public IP shows what the internet sees about you: address, ISP, ASN, approximate location, and whether you’re routing direct or through a tunnel.

  • Cellular shows every line on the phone (dual SIM and eSIM included), radio type, and registration state. It only shows what iOS actually exposes, and I decided early on not to invent signal bars or labels Apple won’t give third-party apps.

  • Diagnostics runs eight timed checks (DNS, TCP, TLS, first byte, and so on) and gives you a plain-language verdict with next steps, not a wall of output.

  • Speed test is a native Swift port of Cloudflare’s open source adaptive speed test methodology. Same endpoints, same approach, no web view.

There’s a private connection timeline that records handoffs and IP changes, and history stays on the device.

Netgleam is $0.99, one time. iPhone and iPad.

What I learned

The photos app took about three months of nights and weekends in the fall of 2025 to get to something I loved using, and then another six months of polish and App Store readiness before I shipped it. Tallyday went from first commit to the App Store in a day and to a solid 1.0 in a couple of weeks. Netgleam went from a design spec to live on the App Store in about twelve days.

That’s not because the later apps are simpler. It’s because the tools got dramatically better and I got better at using them. Codex and Claude Code write the code, review each other’s pull requests, run the tests, generate the App Store screenshots, and handle the release pipeline. My job became deciding what to build, using it every day, and being picky about the details.

The single biggest lesson: build the thing you want to use. On This Day is on my Home Screen because I open it every morning. Netgleam exists because I was annoyed in an airport. That’s a much better filter than any market research.

If you try any of these, I’d love to hear what you think. Every app has a support email that goes to me, and I read all of it. And if you’ve got an idea for something small and useful that should exist on your iPhone, tell me in the comments. The factory has capacity.

Thanks for reading OmarKnows! Subscribe for free to receive new posts and support my work.

Read the whole story
alvinashcraft
43 minutes ago
reply
Pennsylvania, USA
Share this story
Delete

Amazon is trying to crush class-action suits before they get started

1 Share
An illustration of the Amazon logo

On Friday, Amazon customers received an email alerting them to an update to the site's terms and conditions. Most notably, it stated that disputes would now be resolved through arbitration and said users agree to a class action waiver.

Amazon framed this as a "fast and efficient" way to resolve issues, but it notably would prevent customers from seeking the involvement of a judge or jury in most circumstances. Customers can still take Amazon to small claims court in certain circumstances, though payouts are often limited to a few thousand dollars.

The relevant sections of Amazon's legal policies page now read:

YOU AND WE AGREE THAT ANY …

Read the full story at The Verge.

Read the whole story
alvinashcraft
44 minutes ago
reply
Pennsylvania, USA
Share this story
Delete

Bluesky's Active User Base Shrinks 52% Over 18 Months, But Its Protocol is Spreading

1 Share
An anonymous reader shared this report from TechCrunch: According to data from digital intelligence provider Similarweb, Bluesky's mobile app had 10.4 million monthly active users worldwide in June 2026, down 27.2% year-over-year. In addition, mobile daily active users continued to decline, falling 25.6% year-over-year in July to around 3 million... Bluesky's app has lost more than half its monthly active users from its late-2024 high... Its quarterly average was around 22.1 million monthly active users in the fourth quarter of 2024, Similarweb's data indicates, and it declined to 10.7 million in the second quarter of 2026. That's a decline of around 52%. While Bluesky's numbers are down, those who stuck around are committed. Its smaller community remains relatively active, with a stickiness rate (the ratio of daily to monthly active users) of roughly 29% in June, about the same as Threads. For Bluesky's new CEO, Toni Schneider, these numbers may not be as concerning. The company is not entirely focused on making Bluesky (the app) succeed, but on making it possible for the underlying protocol (AT Proto) to power a growing number of social apps, services, and communities. That's something that is now taking place, as projects like BlackSky and Eurosky are growing, while some AT Proto apps like the video-focused Skylight have found early traction, too. In addition, the company has launched new products, like the AI-powered research tool Attie, and is now working on adding support for private data to Bluesky. The latter could generate new interest in Bluesky from a different type of user — those less interested in the public square, and more interested in private networking and communities. But on Threads in July of 2026, daily active users were up 21.3% year-over-year to 147 million, the article points out, and website visits were up 112% year-over-year to 471.6 million. And Threads now appears to have roughly 66% more monthly active users than X.

Read more of this story at Slashdot.

Read the whole story
alvinashcraft
45 minutes ago
reply
Pennsylvania, USA
Share this story
Delete

MCP Wrote Itself the Deprecation Policy It Has Not Written for You

1 Share

Nathan Booker asked me a good question on LinkedIn a couple of weeks back, and it has been sitting in my notebook since. He wondered whether changes to MCP APIs are really breaking changes at all β€” that when your API is agent-facing you can evolve much more quickly, because there is reasoning on the other end that can deal with the change, the way a person deals with a redesigned interface. Traditional breaking change classification, he suggested, may no longer apply. It is the kind of question I like, because the lazy answer is available in both directions and neither one is right. So I went and counted.

The first thing to say is that MCP takes change more seriously than the conversation gives it credit for. The Model Context Protocol has a feature lifecycle and deprecation policy, adopted through its proposal process, and it is genuinely good. Features move through Active, Deprecated, and Removed. There is a minimum twelve-month window between deprecation and the earliest possible removal. The deprecation proposal has to document a migration path, or explicitly state that none is needed, and a named replacement has to already exist before the deprecation lands. There is one canonical page listing everything on its way out and by when. The client libraries are required to mark deprecated surface using the language’s own mechanism, and should emit a runtime warning when you touch it, and a library that keeps failing to do that can be relegated. That is the deprecation practice I have been asking API providers for since 2015. The working group wrote it for itself and shipped it.

Now go one layer down. When you publish a server to the MCP registry you must give it a version string, it has to be unique for that publication, and it is immutable once published. Semantic versioning is recommended. Version ranges are prohibited. All real, all enforced β€” and all of it about identity. It exists so a registry can tell you which publication is which and which one is newest. There is a deprecation status in the registry metadata, which is more than I expected to find, and you can leave a free-text note with it. What there is nowhere to put is a date by which the thing stops working, a structured pointer to what replaced it, or anything scoped smaller than the whole server. You can deprecate a listing. You cannot deprecate a tool.

Go one more layer down, to the tool surface itself β€” which tools exist, what they are called, what their descriptions say β€” and there is nothing. No policy, no field, no convention. Which is unfortunate, because that is the layer an agent actually binds to. A model does not consume your server version. It reads a list of tools with names and descriptions and decides what to call on the strength of that text.

So I counted, to find out whether that gap shows up in behavior. I walked the entire public MCP registry: 66,954 publication records across 20,359 distinct servers. 96.9% of the version strings parse as valid semantic versions. That is near-total compliance with a recommendation that carries no enforcement at all. Whatever else is happening here, this is not a population that ignores versioning guidance. But 59.5% of registered servers published exactly once and never again. Only about one and a half percent ever use the deprecation status that exists. And across 369 first-party MCP server repositories, exactly one mentions a sunset in its README.

People are doing what they were told. The gap is in what they were told. The discipline in this ecosystem degrades in exact proportion to how close you get to the thing the consumer depends on, and that is a very different finding from anybody being careless.

Here is the part the old taxonomy has no name for. A tool description is not documentation. It is input. When you write the description for an MCP tool you are writing part of the prompt that decides whether a model calls it and how. Rewrite that sentence and you have changed the behavior of every agent bound to it β€” and the change produces no schema diff, trips no semantic version rule, fails no lint, and sails through every breaking-change gate anyone has built, because those instruments watch structure and the change was in prose. Of the 61,097 MCP tools I have catalogued, 58,937 carry a prose description. The ungoverned surface is not an edge case. It is nearly the whole thing. minherz wrote this up from production experience better than I can, and his line is the one to keep: that is a breaking change regardless of whether the JSON schema is backward compatible.

Which brings me back to Nathan, and to an answer I did not expect to be so lopsided. He is right that the inherited classification does not fit. He is right that an agent will shrug off a field renamed from qty to quantity in a way that would break every hand-written client ever shipped. But the tolerance runs one direction only. Agents are more forgiving of syntactic change and considerably less forgiving of semantic and economic change than the people they replaced, because a human who hits a 402 or a revoked scope or a surprise rate limit files a ticket, and an agent retries, degrades, or quietly stops producing an outcome somebody is depending on. The changes I have been calling the real breaking changes for years β€” the price, the limit, the terms, the scope you can no longer get β€” get worse under agents, not better, because the party who would have noticed is not a party anymore.

And the strongest argument against evolving faster on the theory that the agents will cope is that MCP itself did not make that argument. At the one layer it actually governs, it chose twelve months, a mandatory migration path, and a runtime warning in your IDE. Nobody in that room said the models would figure it out.

The other thing worth watching is where the market has put its fix. Cloudflare shipped a feature letting portal administrators rename and redescribe tools on servers they do not own, and the reason they gave is that you have no control over how the server author named the tools or wrote the descriptions, and unclear names make it harder for agents. Read that twice. Nobody builds an override layer for metadata that does not matter, so it is independent proof that the prose is load-bearing. But notice which side of the table the fix is on. The thing shipping in production is the consumer overriding what the producer wrote, not the producer telling the consumer anything. That is consumer-side governance showing up on its own because the producer side does not exist, and it is exactly what always happens when producers do not give consumers what they need β€” everyone builds it themselves, one at a time, and the ecosystem ends up with thousands of private forks of your metadata and no shared truth about what your tools do.

I have written up the full guidance, but the short version for anyone running an MCP server today: treat the description as part of the contract and give a description change the notice you would give a schema change. Version the tool surface instead of inheriting a number from your REST API, because your descriptions can be rewritten without your API moving at all β€” the common advice to align those two versions makes the most agent-relevant change class invisible by construction. Removing a tool is the most severe thing you can do. Say what is going away and say when, somewhere a machine can read it. Test behaviorally, because a schema diff provably cannot see this. And carry your pricing and rate limits into the same discipline as your schemas.

None of that needs a new specification to start, and some of it will eventually want one. The registry already crossed from identity into lifecycle the day it added a deprecation status. Giving that status a sunset date, a structured successor, and tool-level granularity is an incremental proposal into a working group that has already proven it will adopt a serious deprecation policy when somebody writes one. That is the difference between saying a thing is broken and doing the work. This is not broken. It is unfinished at exactly one layer, and it happens to be the layer everything else now runs on.



Read the whole story
alvinashcraft
45 minutes ago
reply
Pennsylvania, USA
Share this story
Delete

Build an MCP server in Rust with rmcp: a walk-through πŸ¦€

1 Share

This tutorial walks through building an MCP server in Rust with
rmcp, the official Model Context Protocol Rust SDK.

The example is a real one: a devops agent that manages AWS EC2 G5g instances β€” Graviton2
boxes with NVIDIA T4G GPUs β€” serving Gemma 4 under vLLM. It launches instances, drives them
over SSM, and health-checks the model. There's an existing Python version, so at the end we
can put the two side by side.

Follow along and you'll have a working, registerable MCP server. πŸ¦€

Why Rust for this?

Worth answering properly, because the weak version of the argument is easy to make and easy to
demolish β€” and the real one is better anyway.

Start with what it isn't: these tools are I/O bound. Every one is an AWS API call β€”
describe_instances, send_command, polling SSM β€” so 100–500 ms of network per call. The
caller's language contributes nothing measurable there. Anyone selling you a Rust rewrite on
raw speed for this workload is selling something.

Three claims that don't hold, so nobody has to make them in the comments:

Claim Why it fails
"462 ms startup is slow" stdio servers spawn once per session, not per call
"Rust is faster" the work is network round-trips to AWS
"smaller supply chain" 241 crates vs 34 Python packages β€” it's worse

What actually justifies it, for this codebase:

1. It's a fleet, not a server. This monorepo has 16 rigs, each with its own MCP
server. That changes the units:

All loaded together 🐍 Python πŸ¦€ Rust
Resident memory 16 Γ— 83 MB β‰ˆ 1.33 GB 16 Γ— 12 MB β‰ˆ 192 MB
Session startup 16 Γ— 462 ms β‰ˆ 7.4 s 16 Γ— 2.5 ms β‰ˆ 40 ms

A gigabyte of resident Python to expose sixteen tool lists is a real cost.

2. No shared interpreter. These rigs install system-wide β€” no virtualenvs, by policy β€” so
all sixteen share one Python. Sixteen servers with independently drifting boto3 and mcp
pins in one interpreter is a standing conflict risk. A static binary has no such coupling;
each rig pins whatever it likes in its own Cargo.lock.

3. The schema can't drift from the code. More on this at Step 3, but it's the one that
survives longest: schemars generates the tool schema from the same struct the handler
destructures.

So: distribution and correctness, not speed. βœ… If you have one MCP server and it works,
this is not a reason to rewrite it.

How does this all fit together?

Two halves. The agent and the MCP server run on your machine; the GPU box is remote, and it
has no inbound SSH β€” everything goes through the AWS APIs.

   YOUR MACHINE                                     AWS  us-east-1
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”       β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                              β”‚       β”‚                                       β”‚
β”‚  Claude Code / IDE           β”‚       β”‚  β”Œβ”€ EC2 g5g.4xlarge ───────────────┐  β”‚
β”‚         |                    β”‚       β”‚  β”‚  Graviton2 (aarch64)            β”‚  β”‚
β”‚         | MCP Β· JSON-RPC 2.0 β”‚       β”‚  β”‚  + NVIDIA T4G (SM 7.5)          β”‚  β”‚
β”‚         | over stdio         β”‚       β”‚  β”‚                                 β”‚  β”‚
β”‚         v                    β”‚  EC2  β”‚  β”‚  [PY] vLLM + [RUST] vllm-rs     β”‚  β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚  API  β”‚  β”‚  listening on :8000             β”‚  β”‚
β”‚  β”‚ [RUST]                 │──┼──────>β”‚  β”‚                                 β”‚  β”‚
β”‚  β”‚ gpu-vllm-g5g-2b        β”‚  β”‚       β”‚  β”‚  Gemma 4 E2B                    β”‚  β”‚
β”‚  β”‚                        β”‚  β”‚  SSM  β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
β”‚  β”‚ rmcp 3.1.2             │──┼──────>β”‚           ^                           β”‚
β”‚  β”‚ tokio Β· schemars       β”‚  β”‚  Run  β”‚           |  no inbound SSH,          β”‚
β”‚  β”‚ aws-sdk-ec2 / -ssm     β”‚  β”‚  Cmd  β”‚           |  no key pair,             β”‚
β”‚  β”‚ 1 binary Β· 2.5 ms      β”‚  β”‚       β”‚           |  no port 22 rule          β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚       β”‚                                       β”‚
β”‚        9 tools               β”‚       β”‚  IAM instance profile carries         β”‚
β”‚  list / start / stop /       β”‚       β”‚  AmazonSSMManagedInstanceCore         β”‚
β”‚  terminate / endpoint /      β”‚       β”‚                                       β”‚
β”‚  run_remote / health ...     β”‚       β”‚                                       β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜       β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

The agent never talks to the GPU box directly. It calls a tool; the tool calls EC2 to
manage the instance's lifecycle, or SSM Run Command to execute something on it. That's
what lets the box run with no inbound rules at all β€” which is the main reason this is worth
building as a server rather than a pile of shell scripts.

The [RUST] on the right-hand side is vLLM's own Rust frontend β€” the other article in this
series. This one is the [RUST] on the left: the Rust that drives the box.

What is MCP, in one paragraph?

Model Context Protocol is how an AI agent discovers and calls your tools. Your server
advertises a list of tools with JSON Schemas; the client (Claude Code, an IDE, whatever)
calls them over JSON-RPC 2.0. Transport is usually stdio β€” the client spawns your binary
and talks over stdin/stdout.

That last detail matters for the Rust pitch: if the client spawns your process on every
session, process startup is a user-visible cost.

Step 1 β€” Scaffold

cargo new --bin rust-mcp --name gpu-vllm-g5g-2b-mcp
cd rust-mcp

Now the dependencies. Feature flags are the thing to get right here β€” cargo add rmcp
on its own compiles fine and gives you almost nothing:

cargo add rmcp --features server,macros,transport-io
Feature What it brings
server the ServerHandler trait and router types
macros #[tool], #[tool_router], #[tool_handler]
transport-io stdio transport

The crate also ships client, auth, elicitation, transport-streamable-http-server and
more, all off by default. Add them when you need them.

Then the rest:

cargo add tokio --features rt-multi-thread,macros,process,time
cargo add serde serde_json anyhow schemars
cargo add aws-config aws-sdk-ec2 aws-sdk-ssm aws-sdk-secretsmanager

Resulting Cargo.toml:

[package]
name = "gpu-vllm-g5g-2b-mcp"
version = "0.1.0"
edition = "2024"

[dependencies]
rmcp = { version = "3.1.2", features = ["server", "macros", "transport-io"] }
tokio = { version = "1.53.1", features = ["rt-multi-thread", "macros", "process", "time"] }
aws-config = "1.10.1"
aws-sdk-ec2 = "1.246.0"
aws-sdk-ssm = "1.118.0"
serde = "1.0.229"
serde_json = "1.0.151"
schemars = "1.2.2"
anyhow = "1.0.104"

πŸ”Ž Tip: where the canonical examples live

rmcp moves fast, and rendered docs lag. The vendored tests on your own disk are
compiled against the exact version you resolved:

ls ~/.cargo/registry/src/*/rmcp-3.1.2/tests/

tests/test_tool_macros.rs is a complete, working server in about 60 lines. When an API
question comes up, that file answers it faster and more reliably than anything else. ⚑

Step 2 β€” The server struct

An rmcp server is a struct that owns a ToolRouter:

use rmcp::{
    ErrorData, ServerHandler, ServiceExt,
    handler::server::{router::tool::ToolRouter, wrapper::Parameters},
    model::{CallToolResult, ContentBlock, Implementation, ServerCapabilities, ServerInfo},
    tool, tool_handler, tool_router,
    transport::stdio,
};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};

#[derive(Clone)]
struct G5gServer {
    tool_router: ToolRouter<Self>,
}

Step 3 β€” Describe your inputs as types

This is the part that sold me on the whole exercise. Your tool's input is a plain struct, and
schemars turns it into the JSON Schema the agent sees β€” doc comments and all:

#[derive(Debug, Serialize, Deserialize, JsonSchema)]
struct InstanceId {
    /// EC2 instance id, e.g. `i-0123456789abcdef0`.
    instance_id: String,
}

That doc comment becomes the field's description in the tool schema. Rename the field and
the schema follows. The compiler checks the type your handler destructures. There is no
second artifact to keep in sync. βœ…

Step 4 β€” Write the tools

#[tool_router] on the impl block, #[tool] on each method:

#[tool_router(router = tool_router)]
impl G5gServer {
    fn new() -> Self {
        Self { tool_router: Self::tool_router() }
    }

    #[tool(description = "List EC2 instances tagged ManagedBy=gpu-vllm-g5g-2b.")]
    async fn list_g5g_instances(&self) -> Result<CallToolResult, ErrorData> {
        let conf = aws_config::defaults(aws_config::BehaviorVersion::latest())
            .region(aws_config::Region::new("us-east-1"))
            .load()
            .await;
        let ec2 = aws_sdk_ec2::Client::new(&conf);

        let resp = match ec2.describe_instances()
            .filters(Filter::builder()
                .name("tag:ManagedBy").values("gpu-vllm-g5g-2b").build())
            .send().await
        {
            Ok(r) => r,
            Err(e) => return ok(format!("❌ describe_instances failed: {e}")),
        };

        let mut rows = Vec::new();
        for res in resp.reservations() {
            for inst in res.instances() {
                rows.push(format!("| `{}` | {} | {} |",
                    inst.instance_id().unwrap_or("?"),
                    inst.instance_type().map(|t| t.as_str()).unwrap_or("?"),
                    inst.state().and_then(|s| s.name())
                        .map(|n| n.as_str()).unwrap_or("unknown"),
                ));
            }
        }
        ok(format!("πŸ“‘ Instances\n\n| id | type | state |\n|---|---|---|\n{}",
                   rows.join("\n")))
    }
}

Tools that take arguments wrap them in Parameters<T>:

    #[tool(description = "Terminate a G5g instance. Permanent β€” destroys the root volume.")]
    async fn terminate_g5g_instance(
        &self,
        Parameters(args): Parameters<InstanceId>,
    ) -> Result<CallToolResult, ErrorData> {
        // …
    }

And a small helper, since every tool returns the same shape:

fn ok(text: String) -> Result<CallToolResult, ErrorData> {
    Ok(CallToolResult::success(vec![ContentBlock::text(text)]))
}

Step 5 β€” Implement ServerHandler

#[tool_handler] wires the router in, so you never write a dispatch match:

#[tool_handler(router = self.tool_router)]
impl ServerHandler for G5gServer {
    fn get_info(&self) -> ServerInfo {
        let mut info = ServerInfo::new(
            ServerCapabilities::builder().enable_tools().build()
        );
        info.server_info = Implementation::new(
            "gpu-vllm-g5g-2b", env!("CARGO_PKG_VERSION")
        );
        info.instructions = Some(
            "Devops agent for AWS EC2 G5g (Graviton2 + NVIDIA T4G) serving Gemma 4 \
             under vLLM. Remote administration goes through SSM; there is no inbound SSH."
                .to_string(),
        );
        info
    }
}

πŸ’‘ These model structs are #[non_exhaustive], so use the constructors (ServerInfo::new,
Implementation::new) and then assign fields β€” a struct literal won't compile, even with
..Default::default().

Step 6 β€” main

Four lines:

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let service = G5gServer::new().serve(stdio()).await?;
    service.waiting().await?;
    Ok(())
}
cargo build --release

Step 7 β€” Test the protocol by hand

An MCP server is a protocol implementation, so test it with a protocol transcript. Three
JSON-RPC lines on stdin β€” no client required:

printf '%s\n' \
'{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2026-07-28","capabilities":{},"clientInfo":{"name":"probe","version":"0"}}}' \
'{"jsonrpc":"2.0","method":"notifications/initialized"}' \
'{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}' \
'{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"get_help","arguments":{}}}' \
| ./target/release/gpu-vllm-g5g-2b-mcp
initialize OK: gpu-vllm-g5g-2b 0.1.0 proto 2026-07-28
tools/list OK: 9 tools -> get_deployment_config, get_endpoint, get_help,
  list_g5g_instances, run_remote, start_g5g_instance, stop_g5g_instance,
  terminate_g5g_instance, verify_model_health
tools/call get_help OK

🟒 rmcp 3.1.2 negotiates the 2026-07-28 spec version by default.

Keep this snippet. It's the fastest way to tell "my server is broken" from "my client config
is broken."

Step 8 β€” Register it

Point your MCP client at the binary. For Claude Code, .mcp.json:

{
  "mcpServers": {
    "gpu-vllm-g5g-2b": {
      "command": "/abs/path/to/rust-mcp/target/release/gpu-vllm-g5g-2b-mcp",
      "env": { "AWS_REGION": "us-east-1" }
    }
  }
}

The server name prefixes every tool β€” mcp__gpu-vllm-g5g-2b__list_g5g_instances β€” so name it
after the thing it manages, especially if you run several.

Credentials come from the standard AWS provider chain, so whatever
aws sts get-caller-identity resolves is what the server gets. Set AWS_PROFILE to pick one.

Tests worth writing

The interesting assertions aren't about the code, they're about the machine. Turing has no
bf16 datapath and no fp8, so the serving flags must differ from every L4-class box β€” exactly
the sort of thing that silently reverts when someone copies a flag set from a neighbour:

#[test]
fn serve_flags_are_turing_shaped() {
    let f = serve_flags("google/gemma-4-E2B-it", "g5g.2xlarge");
    assert!(f.contains("--dtype float16"), "Turing has no bf16 datapath");
    assert!(f.contains("--kv-cache-dtype auto"), "Turing has no fp8 datapath");
    assert!(!f.contains("attention-backend"));   // not a real vLLM v0.27 variable
}

#[test]
fn unknown_types_are_rejected_and_never_need_swap() {
    assert!(validate_instance_type("t4g.2xlarge").is_err());  // burstable CPU box, no GPU
    assert!(!needs_swap("t4g.2xlarge"));                      // 0 GiB must not read as "tiny"
}

That second one earns its keep: host_memory_gb returns 0 for an unknown instance type,
and a naive ram < 16 would decide an unrecognised machine needs a swapfile.

running 5 tests
test result: ok. 5 passed; 0 failed; finished in 0.00s

🐍 vs πŸ¦€ β€” the scoreboard

Cold start measured the way a client experiences it: spawn the process, send initialize +
initialized + tools/list, stop the clock when the tool list comes back. Seven runs, median.

🐍 Python (FastMCP) πŸ¦€ Rust (rmcp)
Cold start to tools/list 462.0 ms (437–530) 2.5 ms (1.8–2.9)
Peak RSS 83 MB 12 MB
Artifact Python runtime + 34 packages one binary, 39.4 MB (19.9 MB stripped)
Direct dependencies 3 14
Total resolved packages 34 241
Tools implemented 15 9
Source 759 lines 560 + 52 of tests
Clean release build n/a 5 m 28 s

185x on cold start β€” but per the section up top, resist quoting that on its own. One
server spawned once a session, 460 ms, nobody notices. It only becomes a number worth having
when you multiply it by sixteen rigs, and even then it's the 12 MB vs 83 MB row that does
the heavier lifting.

Read the rest honestly too. 241 resolved packages against 34 means the static binary is
not a smaller supply chain, just the same one audited in Cargo.lock. The port covers 9
tools to Python's 15 β€” the provisioning path (cloud-init rendering, AMI resolution, spot
options) is the fiddly half and isn't ported. And 5 m 28 s of clean build against an
interpreter that starts instantly is a real cost while you're iterating. πŸ“Š

So, worth it?

For a single MCP server that already works: no. Don't rewrite it.

For sixteen of them sharing one system Python, shipped to machines that shouldn't need a
Python environment at all: yes β€” and note that neither half of that sentence is about speed.
It's a packaging answer.

The part that'll still be true next year is schemars. The tool schema the agent sees is
generated from the same struct the handler destructures, checked by the compiler, documented
by the doc comments on its fields. In the Python version the schema, the runtime types and the
docs are three artifacts that agree by convention β€” and go quiet when they stop agreeing.

The 462 ms is a bonus. The schema not being able to lie about the code is the reason. βœ…

Cheat sheet

# scaffold
cargo new --bin my-mcp && cd my-mcp
cargo add rmcp --features server,macros,transport-io
cargo add tokio --features rt-multi-thread,macros
cargo add serde serde_json schemars anyhow

# canonical examples for YOUR resolved version
ls ~/.cargo/registry/src/*/rmcp-*/tests/test_tool_macros.rs

# build + smoke test
cargo build --release
printf '%s\n' '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2026-07-28","capabilities":{},"clientInfo":{"name":"p","version":"0"}}}' \
              '{"jsonrpc":"2.0","method":"notifications/initialized"}' \
              '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}' \
  | ./target/release/my-mcp

Four macros to remember: #[tool_router] on the impl, #[tool] on each method,
#[tool_handler] on the ServerHandler impl, Parameters<T> around your input struct.

Rust 1.97.1, rmcp 3.1.2, aws-sdk-ec2 1.246.0, edition 2024. Startup measured on the dev host
β€” it's a comparison of two MCP servers, not a hardware result. Single machine, seven runs per
side, median reported.

Read the whole story
alvinashcraft
45 minutes ago
reply
Pennsylvania, USA
Share this story
Delete

Random.Code() - Upgrading Rocks to .NET 11

1 Share
From: Jason Bock
Duration: 1:46:34
Views: 9

Will .NET 11 break Rocks? Is there a new feature that will melt my source generation? Only one way to find out!

https://github.com/JasonBock/Rocks/issues/425

#dotnet #csharp

Read the whole story
alvinashcraft
46 minutes ago
reply
Pennsylvania, USA
Share this story
Delete
Next Page of Stories