<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Jui-Hung Yuan</title>
    <description>The latest articles on DEV Community by Jui-Hung Yuan (@juihungyuan).</description>
    <link>https://dev.to/juihungyuan</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3754943%2F401003a3-ba60-4275-978a-a42377e5a3b9.jpg</url>
      <title>DEV Community: Jui-Hung Yuan</title>
      <link>https://dev.to/juihungyuan</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/juihungyuan"/>
    <language>en</language>
    <item>
      <title>I tried to find emotion in my own speech using TRIBE v2</title>
      <dc:creator>Jui-Hung Yuan</dc:creator>
      <pubDate>Fri, 14 Aug 2026 10:54:06 +0000</pubDate>
      <link>https://dev.to/juihungyuan/i-tried-to-find-emotion-in-my-own-speech-using-tribe-v2-2on3</link>
      <guid>https://dev.to/juihungyuan/i-tried-to-find-emotion-in-my-own-speech-using-tribe-v2-2on3</guid>
      <description>&lt;p&gt;The bigger aim is to use TRIBE v2 to improve my speaking. Before that can work,&lt;br&gt;
I need to know whether the model captures anything about &lt;strong&gt;emotion, attention or&lt;br&gt;
social connection&lt;/strong&gt; — or only the sound of the words.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why TRIBE v2
&lt;/h2&gt;

&lt;p&gt;It maps video, audio and text onto brain activity with state-of-the-art&lt;br&gt;
accuracy, and it trains with &lt;strong&gt;modality dropout (p = 0.3)&lt;/strong&gt;, so it still&lt;br&gt;
predicts sensibly when a modality is missing — useful when all I have is audio.&lt;/p&gt;

&lt;p&gt;One constraint shaped everything below. The &lt;strong&gt;amygdala&lt;/strong&gt; — the region an emotion&lt;br&gt;
study most wants — sits in subcortex, and the paper reports subcortical accuracy&lt;br&gt;
is &lt;em&gt;"two to three folds"&lt;/em&gt; lower than cortical. So the most relevant region is&lt;br&gt;
also the least reliably predicted, and this whole analysis runs on the cortical&lt;br&gt;
surface instead.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Focus question:&lt;/strong&gt; can I find a brain region whose predicted activity lets me&lt;br&gt;
say, reliably, &lt;em&gt;"this passage is emotional"&lt;/em&gt;?&lt;/p&gt;
&lt;h2&gt;
  
  
  Data
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;one of my own Toastmasters speeches (7.5 min)&lt;/li&gt;
&lt;li&gt;two Toastmasters World Championship finalists — the 2025 winner and the 2017
third place (7.8 and 8.1 min)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Audio only, 16 kHz mono.&lt;/p&gt;
&lt;h2&gt;
  
  
  Architecture in one paragraph
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Three modalities, &lt;strong&gt;four&lt;/strong&gt; frozen encoders: Llama-3.2-3B for text, w2v-BERT-2.0
for audio, and V-JEPA2-giant plus DINOv2-large for video. &lt;/li&gt;
&lt;li&gt;Each is compressed to
384 dimensions by a trainable linear layer, concatenated into a
&lt;strong&gt;3 × 384 = 1152&lt;/strong&gt;-dimensional vector&lt;/li&gt;
&lt;li&gt;passed through an 8-layer transformer
over 100-second windows so the modalities can exchange information&lt;/li&gt;
&lt;li&gt;A final
linear readout maps those 1152 dimensions onto 20,484 cortical vertices at 1 Hz.
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[ Input Video ] ───► [ Visual Track ] ───► DINOv2 + V-JEPA2 ───┐
                                                               │ ──► [ Unified Transformer ] ──► [ Cortical fMRI Prediction ]
                     [ Audio Track ]  ───► Wav2Vec-BERT     ───┤
                     [ Text Track ]   ───► LLaMA 3.2        ───┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h2&gt;
  
  
  Method — two angles
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Step 0&lt;/strong&gt; was standing up inference on an AWS &lt;code&gt;g6.xlarge&lt;/code&gt; (one L4, 24 GB) —&lt;br&gt;
enough for all backbones, since features are cached per modality so only one&lt;br&gt;
encoder is resident at a time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;First approach — exploratory.&lt;/strong&gt; Look at the parcels the literature ties to&lt;br&gt;
social and emotional inference — temporo-parietal junction (&lt;code&gt;PGi&lt;/code&gt;) and middle&lt;br&gt;
temporal gyrus (&lt;code&gt;TE1a&lt;/code&gt;) — and check whether they rise during passages I know are&lt;br&gt;
emotional.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Second approach — statistical.&lt;/strong&gt; Hand-label every speech by ear on two&lt;br&gt;
independent dimensions, affect (emotional / neutral) and discourse (narrative /&lt;br&gt;
argument). Compute loudness, pitch variability and speech rate per second. Fit&lt;br&gt;
all of them &lt;strong&gt;together&lt;/strong&gt; against each of the 360 Glasser parcels, and test each&lt;br&gt;
contrast by shuffling which segments carry which label, 10,000 times.&lt;/p&gt;

&lt;p&gt;Labels are just Audacity label-track exports — start, end, label:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csvs"&gt;&lt;code&gt;&lt;span class="mf"&gt;15.069025&lt;/span&gt;   &lt;span class="mf"&gt;28.128847&lt;/span&gt;   &lt;span class="k"&gt;neutral&lt;/span&gt;
&lt;span class="mf"&gt;29.803183&lt;/span&gt;   &lt;span class="mf"&gt;46.881412&lt;/span&gt;   &lt;span class="k"&gt;neutral&lt;/span&gt;
&lt;span class="mf"&gt;56.257694&lt;/span&gt;   &lt;span class="mf"&gt;73.001056&lt;/span&gt;   &lt;span class="k"&gt;emotional&lt;/span&gt;
&lt;span class="mf"&gt;86.060878&lt;/span&gt;   &lt;span class="mf"&gt;97.781231&lt;/span&gt;   &lt;span class="k"&gt;neutral&lt;/span&gt;
&lt;span class="mf"&gt;125.575211&lt;/span&gt;  &lt;span class="mf"&gt;147.006713&lt;/span&gt;  &lt;span class="k"&gt;emotional&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Results
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The exploratory pass died early
&lt;/h3&gt;

&lt;p&gt;TPJ and MTG turn out to track &lt;strong&gt;language&lt;/strong&gt; regions at r = 0.80–0.90 — their&lt;br&gt;
apparent prominence is inherited, not independent. Nothing cleanly marked jokes&lt;br&gt;
landing, arguments convincing, or emotional build-ups.&lt;/p&gt;

&lt;p&gt;Is that bad? Not for the model — TPJ genuinely is part of the language network,&lt;br&gt;
so this may be biologically correct. It is bad for &lt;em&gt;my&lt;/em&gt; question: if TPJ is&lt;br&gt;
80–90% predictable from language cortex, it cannot serve as independent evidence&lt;br&gt;
that something social or emotional happened.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkhk2qsfv0taf6wudc0dw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkhk2qsfv0taf6wudc0dw.png" alt="Carpet plot of my own speech" width="800" height="304"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Twelve region groups over 450 seconds of my own speech. Red is above this&lt;br&gt;
clip's average, blue below. The auditory and language rows move; the emotion&lt;br&gt;
rows (interoceptive insula, vmPFC) sit nearly flat throughout.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Emotion: zero parcels
&lt;/h3&gt;

&lt;p&gt;Once loudness, pitch and speech rate were accounted for, &lt;strong&gt;no parcel&lt;/strong&gt; showed a&lt;br&gt;
reliable emotional-vs-neutral difference in any of the three speeches.&lt;/p&gt;

&lt;p&gt;More telling is which predictors actually moved the predictions:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;predictor&lt;/th&gt;
&lt;th&gt;mean |β| across 360 parcels&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;speech rate&lt;/td&gt;
&lt;td&gt;0.036&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;loudness&lt;/td&gt;
&lt;td&gt;0.034&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;emotion label&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.021&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two things I can measure from the waveform in ten lines of numpy move the&lt;br&gt;
predicted brain activity &lt;strong&gt;more than what I heard as emotional content&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Separately, a PCA across region groups showed a &lt;strong&gt;single component explaining&lt;br&gt;
55–59%&lt;/strong&gt; of the variance — twelve "networks" with roughly three degrees of&lt;br&gt;
freedom between them. Regions move together far more than they move apart.&lt;/p&gt;

&lt;h3&gt;
  
  
  Discourse: 42 parcels, and why I don't believe them
&lt;/h3&gt;

&lt;p&gt;Narrative-vs-argument looked more promising — 42 parcels survived in two of the&lt;br&gt;
three speeches. Then I looked at which ones: primary motor, premotor,&lt;br&gt;
somatosensory, superior parietal, visual cortex. Nothing to do with discourse.&lt;/p&gt;

&lt;p&gt;The reason showed up in a whole-brain check. In those two speeches the &lt;em&gt;entire&lt;/em&gt;&lt;br&gt;
cortex shifted between narrative and argument blocks:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;speech&lt;/th&gt;
&lt;th&gt;global shift (narr − argu)&lt;/th&gt;
&lt;th&gt;% of all 360 parcels negative&lt;/th&gt;
&lt;th&gt;"significant" parcels&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;2025 champion&lt;/td&gt;
&lt;td&gt;−0.044&lt;/td&gt;
&lt;td&gt;79%&lt;/td&gt;
&lt;td&gt;91&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2017 third place&lt;/td&gt;
&lt;td&gt;−0.079&lt;/td&gt;
&lt;td&gt;86%&lt;/td&gt;
&lt;td&gt;114&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;my speech&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;+0.031&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;41%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The two competition speeches show a brain-wide offset; mine doesn't. And the&lt;br&gt;
"significant" parcels appear exactly where the offset appears. They aren't&lt;br&gt;
regions that respond to storytelling — they're simply the &lt;strong&gt;quietest&lt;/strong&gt; parcels,&lt;br&gt;
where a brain-wide shift was big enough to clear the noise bar first. My own&lt;br&gt;
speech, with no such shift, produced nothing at all, which is what exposed the&lt;br&gt;
artifact.&lt;/p&gt;

&lt;p&gt;I tested and rejected the obvious causes: slow drift over the clip (r = −0.04&lt;br&gt;
and −0.19 against time), loudness (r = +0.09, +0.03) and speech rate&lt;br&gt;
(r = −0.08, −0.17). Where the offset comes from is still open.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;I could not find an emotion-triggered signal in TRIBE v2's predicted brain&lt;br&gt;
activity.&lt;/strong&gt; Possible reasons, most defensible first:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Not enough data.&lt;/strong&gt; I planted effects of known size into the real
predictions and raised them until they cleared the significance bar. That
took an effect about &lt;strong&gt;1.3× a parcel's own natural fluctuation&lt;/strong&gt; — larger
than everything the region normally does on its own. Three speeches cannot
reach that, so this is a null from lack of power, not evidence of absence.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Emotion may simply be subtler in predicted activity&lt;/strong&gt; than auditory and
linguistic responses, which dominate everything measured here.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The model predicts a normative response.&lt;/strong&gt; It has no listener state — the
same audio always produces the same prediction — while real emotional
reactions vary enormously between people.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Time resolution is poor.&lt;/strong&gt; The haemodynamic response is slow and stays
elevated after a peak, so sharp emotional moments smear into their
surroundings. There is one sample per second but only a few seconds of real
temporal precision.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;My labels may be too coarse.&lt;/strong&gt; "Emotional" spans humour, pathos and
urgency, which plausibly engage different systems and may cancel when pooled
into one condition.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The thing I'd actually tell someone else
&lt;/h2&gt;

&lt;p&gt;The most useful thing I learned wasn't a finding. It was this:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If the whole brain moves a little, a statistical test will hand you a short&lt;br&gt;
list of regions and call them significant.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In one speech that list was 114 regions long. Every one passed a permutation&lt;br&gt;
test. They looked like a result — until I checked the other 246 parcels and&lt;br&gt;
found they had &lt;em&gt;all&lt;/em&gt; shifted the same way. The 114 weren't the regions that&lt;br&gt;
responded; they were just the &lt;strong&gt;quietest&lt;/strong&gt; ones, where a small brain-wide nudge&lt;br&gt;
was easiest to detect. The test was working correctly. It was answering "is this&lt;br&gt;
region different?" when the honest question was "is this region different &lt;em&gt;from&lt;br&gt;
the rest of the brain&lt;/em&gt;?"&lt;/p&gt;

&lt;p&gt;So: &lt;strong&gt;when a result appears, look at what didn't make the list too.&lt;/strong&gt; If&lt;br&gt;
everything moved, nothing did.&lt;/p&gt;

</description>
      <category>machinelearning</category>
      <category>neuroscience</category>
      <category>datascience</category>
      <category>python</category>
    </item>
    <item>
      <title>Event-Driven AI Orchestration: Local-First Smart Home Automation</title>
      <dc:creator>Jui-Hung Yuan</dc:creator>
      <pubDate>Tue, 10 Mar 2026 14:25:07 +0000</pubDate>
      <link>https://dev.to/juihungyuan/my-apartment-now-dims-the-lights-on-guests-on-purpose-598l</link>
      <guid>https://dev.to/juihungyuan/my-apartment-now-dims-the-lights-on-guests-on-purpose-598l</guid>
      <description>&lt;p&gt;In my previous blog, I built a &lt;a href="https://dev.to/juihungyuan/from-local-to-cloud-what-i-learned-building-a-remote-mcp-server-on-aws-for-smart-home-control-3a24"&gt;remote MCP server connected to Claude so I could control my smart light bulb by just sending a chat message&lt;/a&gt;. It worked! But looking back, the architecture was a glorious overkill.&lt;/p&gt;

&lt;p&gt;All of this, for two people who just want to dim the lights in the evening 🫠..&lt;/p&gt;

&lt;p&gt;Around the same time, I had been hearing a lot about the trend of “Skills over MCP” and the hype around OpenClaw. OpenClaw is popular because it’s a local-first AI assistant — the whole orchestration runs on your own devices, no cloud infrastructure needed, just enough flexibility to build something genuinely useful. That sounded like exactly the right learning opportunity. &lt;/p&gt;

&lt;p&gt;So I decided to build an &lt;strong&gt;OpenClaw-inspired personal assistant&lt;/strong&gt;  for smarthome control — simpler, local, and tailored to just what I needed.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhlgg1i0pi8klv4al74hz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhlgg1i0pi8klv4al74hz.png" alt="light_schedule_illustratation" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Spoiler⚠️&lt;/strong&gt;: it worked out well. My partner and I can now control the light bulb from Slack, and we can schedule light adjustments via chat. I am obsessed with healthy lifestyle. So naturally, I scheduled the light to slowly dim itself over the evening. It’s very peaceful. The only unintended side effect is that when we have guests over, the room gets darker and darker as the night goes on, and somehow they always end up leaving earlier than planned. I feel a little bad about it. But also — they’re getting more sleep now, so really, I’m doing them a favor.&lt;/p&gt;

&lt;h2&gt;
  
  
  OpenClaw basics
&lt;/h2&gt;

&lt;p&gt;To understand how OpenClaw works, I reimplemented its &lt;strong&gt;4 core components&lt;/strong&gt; with the help of Claude — purely for learning. No shortcuts, no copy-paste. Just building each piece by hand until it clicked.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Beauty is in the eye of the beholder — or as the teacher in Spy x Family would say, &lt;strong&gt;🕺ELEGANTO🕺&lt;/strong&gt;. In the following sections, I'll share what I learned from each component, the key insights that surprised me, and what I find elegant about the design.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7enerdw4ywh7t8a4lx1o.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7enerdw4ywh7t8a4lx1o.jpg" alt="alt image" width="686" height="386"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Memory system
&lt;/h3&gt;

&lt;p&gt;OpenClaw keeps the assistant’s memory as simple Markdown files on disk. The assistant’s personality and tone live in &lt;code&gt;SOUL.md&lt;/code&gt;, user preferences in &lt;code&gt;USER.md&lt;/code&gt;, and notable conversations get appended to a daily log. During a conversation, the agent can selectively write to these files, and the system prompt guides which information goes where. It’s not over-engineered — just enough structure to make memory feel real and &lt;strong&gt;🕺ELEGANTO🕺&lt;/strong&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;🤓 In my implementation, this is exposed via two tools: &lt;code&gt;memory_search&lt;/code&gt; (hybrid keyword + vector search over past logs) and &lt;code&gt;memory_write&lt;/code&gt; (append or overwrite a memory file).&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  2. Skill registry
&lt;/h3&gt;

&lt;p&gt;Skills give the agent new capabilities in a Markdown-based format — like a cheatsheet it can pull up on demand. What I find &lt;strong&gt;🕺ELEGANTO🕺&lt;/strong&gt; is the &lt;strong&gt;progressive disclosure&lt;/strong&gt; mechanism. At startup, the agent only sees a one-line summary of each available skill. When it needs to use one, it calls &lt;code&gt;describe_skill&lt;/code&gt; to load the full cheatsheet — actions, parameters, examples — and that doc gets injected into the system prompt for the rest of the session. This keeps the context lean until it’s actually needed, and the same pattern naturally extends to revealing new tools or even new skills over time.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;🤓 In my implementation, each skill is a folder under &lt;code&gt;skills/&lt;/code&gt; with a &lt;code&gt;SKILL.md&lt;/code&gt; file (frontmatter = summary, body = full docs) and a Python script that exposes an &lt;code&gt;execute(action, params)&lt;/code&gt; function. The agent calls &lt;code&gt;describe_skill&lt;/code&gt; first, then &lt;code&gt;execute_skill&lt;/code&gt;. My light bulb skill lives in &lt;code&gt;src/smarthome/agent/skills/light-control&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  3. Scheduled jobs (CRON and Heartbeat)
&lt;/h3&gt;

&lt;p&gt;Beyond just responding to messages, OpenClaw can also proactively run tasks on a schedule. CRON-style jobs fire at specific times, while the Heartbeat is better suited for background monitoring — it batches tasks together so they can share context and inform each other’s results.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;🤓 My implementation is closer to CRON: the scheduler wakes up on a regular interval, checks if any task’s scheduled time falls within the elapsed window, and fires it directly — no LLM call involved. That works &lt;strong&gt;🕺ELEGANTO🕺&lt;/strong&gt; enough for simple device commands like dimming the light at 9pm. If I wanted smarter tasks (e.g. checking my calendar and sending a notification), it would need to trigger an actual agent turn. Tasks are managed via the &lt;code&gt;schedule_task&lt;/code&gt; tool (add/remove/list) and persist in &lt;code&gt;SCHEDULE.md&lt;/code&gt; across restarts.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  4. Channel Adapter
&lt;/h3&gt;

&lt;p&gt;OpenClaw supports many messaging apps — WhatsApp, Telegram, Slack, Discord, and more. The channel adapter is the translator between a specific app and the agent: it normalizes incoming messages into a unified format, and formats the agent’s response back into whatever the app expects. It also acts as the authentication boundary — rather than managing per-user credentials like MCP does, OpenClaw’s approach is perimeter-based: “as long as you can reach me, I trust you.”&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;🤓 I only implemented Slack, which supports both direct messages and &lt;code&gt;@mentions&lt;/code&gt; in channels. One part I find particularly &lt;strong&gt;🕺ELEGANTO🕺&lt;/strong&gt; is the color picker: when the user wants to change the bulb color, a &lt;code&gt;show_palette&lt;/code&gt; action returns a Slack dropdown block. The agent loop intercepts this block and passes it directly to the Slack adapter, which renders it as an interactive dropdown — no extra roundtrip needed. The user picks a color, and it triggers &lt;code&gt;set_color&lt;/code&gt; directly.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Key learning
&lt;/h2&gt;

&lt;p&gt;After building this, my biggest takeaway is that all these components — memory, skills, scheduling — work as well as they do because of how good modern LLMs are at tool calling. Once you wrap the right functionality as tools, the agent just... uses them. Memory feels persistent, skills feel modular, scheduling feels effortless. Especially the skill system: because you can add a new folder with a Markdown file and a Python script and the agent picks it up automatically — that expandability is truly &lt;strong&gt;🕺ELEGANTOOOO🕺&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;One more thing I want to share: I used Claude Code throughout this build, and it went better when I slowed down at the start. My first instinct was to clone the OpenClaw repo, dump it into context, and ask Claude to plan everything at once. That didn’t go well — I didn’t know enough about OpenClaw myself to tell if the plan made sense, and Claude can’t read your mind about what trade-offs matter to you. What actually helped was spending time upfront to clarify scope and intent together, before handing off any implementation. The clearer the brief, the better the output. That’s not specific to Claude — it’s just good collaboration.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick start for everyone
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# Clone the repo&lt;/span&gt;
git clone https://github.com/jui-hung-yuan/smarthome-mcp-lab
cd smarthome-mcp-lab

&lt;span class="gh"&gt;# Add your Anthropic API key&lt;/span&gt;
 mkdir -p ~/.smarthome
 echo 'ANTHROPIC_API_KEY=sk-...' &amp;gt;&amp;gt; ~/.smarthome/.env

&lt;span class="gh"&gt;# Seed memory files (optional but recommended)&lt;/span&gt;
 mkdir -p ~/.smarthome/memory
 echo "# Memory" &amp;gt; ~/.smarthome/memory/MEMORY.md
 echo "# User Preferences" &amp;gt; ~/.smarthome/memory/USER.md

&lt;span class="gh"&gt;# Run with mock bulb (no hardware needed)&lt;/span&gt;
uv run python -m smarthome.agent --mock

&lt;span class="gh"&gt;# Run with real bulb — add `TAPO_USERNAME`, `TAPO_PASSWORD`, `TAPO_IP_ADDRESS` to `~/.smarthome/.env`, then:&lt;/span&gt;
uv run python -m smarthome.agent

&lt;span class="gh"&gt;# Run as a Slack bot — add `SLACK_BOT_TOKEN`, `SLACK_APP_TOKEN`, `SLACK_SIGNING_SECRET` to `~/.smarthome/.env`, then:&lt;/span&gt;
uv run python -m smarthome.agent --slack --mock   # mock bulb
uv run python -m smarthome.agent --slack          # real bulb
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Thanks for reading! 🙏 This was an &lt;strong&gt;🕺ELEGANTO🕺&lt;/strong&gt; project to build and to write about. If you've built something similar, or have thoughts on the architecture, I'd love to hear from you — drop a comment below or open an issue on the repo. Always happy to exchange ideas.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>openclaw</category>
      <category>sideprojects</category>
    </item>
    <item>
      <title>From Local to Cloud: What I Learned Building a Remote MCP Server on AWS for Smart Home Control</title>
      <dc:creator>Jui-Hung Yuan</dc:creator>
      <pubDate>Fri, 20 Feb 2026 14:21:59 +0000</pubDate>
      <link>https://dev.to/juihungyuan/from-local-to-cloud-what-i-learned-building-a-remote-mcp-server-on-aws-for-smart-home-control-3a24</link>
      <guid>https://dev.to/juihungyuan/from-local-to-cloud-what-i-learned-building-a-remote-mcp-server-on-aws-for-smart-home-control-3a24</guid>
      <description>&lt;p&gt;I wanted to tell Claude to turn off my bedroom light. Not just from my laptop at home — but from &lt;strong&gt;anywhere&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyxophwtvufsya3j9mgb5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyxophwtvufsya3j9mgb5.png" alt="illustration of smart home control" width="800" height="589"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;What started as "&lt;em&gt;let's try MCP&lt;/em&gt;" became "&lt;em&gt;why does OAuth keep failing&lt;/em&gt;" and "&lt;em&gt;how do cloud services reach devices behind my router&lt;/em&gt;".&lt;/p&gt;

&lt;p&gt;This post walks through the architecture decisions and the stuff that tripped me up. Not every choice was obvious, and some things only made sense after I'd already built them wrong once.&lt;/p&gt;

&lt;p&gt;The full code is &lt;a href="https://github.com/jui-hung-yuan/smarthome-mcp-lab" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why I Built This
&lt;/h2&gt;

&lt;p&gt;I recently attended a session organized by the Berlin AWS User Group about Amazon Bedrock AgentCore (&lt;em&gt;shoutout to the organizers — the session was really helpful!!&lt;/em&gt;). I'd been using MCP for a while but never built one myself. When I looked around, most posts covered the concept and building local MCP servers — not much about &lt;strong&gt;deploying remote MCP servers to the cloud&lt;/strong&gt; or what actually trips you up when you build one. So I picked a concrete use case: control my TAPO smart light bulb via Claude, and build the whole thing on AWS.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The goal was simple and deliberately small. &lt;strong&gt;One bulb. A handful of tools. But with enough real infrastructure to actually learn from.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Phase 1: Local Prototype with FastMCP
&lt;/h2&gt;

&lt;p&gt;I started with &lt;a href="https://gofastmcp.com" rel="noopener noreferrer"&gt;FastMCP&lt;/a&gt; and Claude Desktop. Getting a working local MCP server took one afternoon.&lt;/p&gt;

&lt;p&gt;The developer experience is genuinely impressive. You define a tool like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@app.tool&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;turn_on&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Turn on the TAPO smart light bulb.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;bulb&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;get_bulb&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;bulb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;turn_on&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Light turned on&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. FastMCP reads your function and auto-generates everything Claude needs — the tool name, description, and input schema. You write the function. FastMCP handles the rest.&lt;/p&gt;

&lt;p&gt;Claude Desktop runs the FastMCP server as a local subprocess. The full path is:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fuxnyqaq5uvzx91tvy7nt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fuxnyqaq5uvzx91tvy7nt.png" alt="local MCP" width="800" height="71"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It worked. I could chat with Claude and control my light. But then I left home.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Turning Point
&lt;/h2&gt;

&lt;p&gt;Claude Desktop's MCP only works with local subprocesses. The moment you close your laptop or step outside, it's gone. I wanted the Claude web app to work too — partly because it's more convenient, partly because building the remote version is where the real learning happens.&lt;/p&gt;

&lt;p&gt;That's a fundamentally different problem. &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Claude web app needs a &lt;strong&gt;publicly accessible&lt;/strong&gt; MCP server with &lt;strong&gt;proper authentication&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;MCP server somehow needs to &lt;strong&gt;reach a device on your local network&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is where AWS comes in.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Architecture
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0bzm0ktawd121c5l2rdn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0bzm0ktawd121c5l2rdn.png" alt="Remote MCP" width="800" height="620"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Each component has a specific job:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cognito&lt;/strong&gt;: Handles user login.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AgentCore Gateway&lt;/strong&gt;: The MCP server.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lambda&lt;/strong&gt;: Serverless handler (No always-on server to manage).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;IoT Core&lt;/strong&gt;: Cloud-to-device message broker.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Local Bridge&lt;/strong&gt;: Runs at home and controls the bulb.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Key Lesson 1: Why AWS IoT Core (and not just Lambda → HTTP)?
&lt;/h2&gt;

&lt;p&gt;AWS IoT Core is a managed cloud service that acts as a message broker between cloud services and physical devices. It uses &lt;strong&gt;MQTT&lt;/strong&gt; — a lightweight protocol designed for devices with unreliable connections — to route messages through a publish/subscribe model. &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Think of it like a radio station: Lambda broadcasts on a channel, and any device tuned to that channel receives the message.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The obvious question: &lt;em&gt;why not have Lambda call the local bridge directly over HTTP?&lt;/em&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  1. NAT Traversal — Lambda can't reach your home network
&lt;/h4&gt;

&lt;p&gt;Lambda lives in AWS. Your home bridge lives behind your router. Your router blocks all inbound connections — Lambda has no address to call. To make direct HTTP work, you'd need a static IP and port forwarding (security risk), a VPN tunnel (operational overhead), or a reverse tunnel like ngrok (fragile, costs money).&lt;/p&gt;

&lt;p&gt;IoT Core flips the direction. Your local bridge reaches out to AWS and holds a persistent MQTT connection open. Lambda publishes to a topic, IoT Core delivers it over that already-open connection. Your home network never needs a public address.&lt;/p&gt;

&lt;h4&gt;
  
  
  2. Cost and efficiency — MQTT is event-driven, HTTP requires polling
&lt;/h4&gt;

&lt;p&gt;With HTTP, if Claude wants to know the current brightness, Lambda would need to make a request every time — or poll regularly to keep state fresh. That's an HTTP call (and cost) for every status check.&lt;/p&gt;

&lt;p&gt;With MQTT, the bridge reports state changes to IoT Core's Device Shadow automatically. Claude asks for brightness? The Shadow answers instantly from cache. No new request needed. The bridge only sends updates when something actually changes.&lt;/p&gt;

&lt;h4&gt;
  
  
  3. Resilience — MQTT handles disconnections automatically
&lt;/h4&gt;

&lt;p&gt;HTTP assumes both sides are reliably reachable. If your bridge restarts or your internet hiccups during a Lambda call, the request just fails.&lt;/p&gt;

&lt;p&gt;MQTT is designed for unreliable connections. If your bridge goes offline, IoT Core queues messages. When it reconnects, pending commands are delivered automatically. No retry logic to write, no state to track manually.&lt;/p&gt;




&lt;h2&gt;
  
  
  Key Lesson 2: FastMCP vs. AgentCore Gateway
&lt;/h2&gt;

&lt;p&gt;Amazon Bedrock AgentCore Gateway is a fully managed service that turns your backend functions into an MCP-compliant server that AI clients like Claude can talk to. It handles OAuth authentication, protocol translation, and tool discovery — so you only write business logic. &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Think of it as the bouncer and translator standing between Claude and your Lambda: it checks credentials, speaks MCP fluently, and routes the right instructions inward.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Here's how it compares to FastMCP as an MCP hosting approach:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Aspect&lt;/th&gt;
&lt;th&gt;Winner&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Tool Schema&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Tie🤝&lt;/td&gt;
&lt;td&gt;FastMCP auto-generates from decorators (better DX). AgentCore requires explicit JSON (&lt;em&gt;~70 lines for 4 tools&lt;/em&gt;), but makes the contract reviewable.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Hosting&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;AgentCore🏆&lt;/td&gt;
&lt;td&gt;FastMCP needs a persistent runtime (container, EC2, Fargate) — costs money even when idle. AgentCore is serverless — Lambda only runs when invoked.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Auth Support&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Tie🤝&lt;/td&gt;
&lt;td&gt;Both handle OAuth well now (FastMCP 2.11+ added JWT, OAuth proxy, full OAuth server).&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Key Lesson 3: OAuth — The Part That Actually Took Time
&lt;/h2&gt;

&lt;p&gt;If I'm honest, the infrastructure was the easy part. Authentication is where I spent most of my debugging time.&lt;/p&gt;

&lt;p&gt;The problem started with how I set up my Cognito configuration. It defaulted to the &lt;code&gt;client_credentials&lt;/code&gt; flow — machine-to-machine (M2M) auth where a service exchanges a client ID and secret directly for a token. No login page, no user interaction.&lt;/p&gt;

&lt;p&gt;That works fine for service-to-service communication. But the Claude web app is a browser-based client. It needs to redirect the user to a login page, have them authenticate, and receive an authorization code back — the &lt;code&gt;authorization_code&lt;/code&gt; flow. These are fundamentally different OAuth patterns. It took me a while to figure out I was using the wrong flow entirely.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Again think of it like a nightclub with a strict bouncer. The bouncer (&lt;strong&gt;AgentCore Gateway&lt;/strong&gt;) doesn't know you personally, but trusts the ID checker down the street (&lt;strong&gt;Cognito&lt;/strong&gt;). You walk down to &lt;strong&gt;Cognito&lt;/strong&gt;, prove who you are, and &lt;strong&gt;Cognito&lt;/strong&gt; gives you a wristband. You bring that wristband back to the bouncer at &lt;code&gt;/auth_callback&lt;/code&gt;, and now you're in.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That callback address is the key. The &lt;code&gt;authorization_code&lt;/code&gt; flow exists precisely because Claude (the browser client) needs a human to authenticate interactively. The code is the shop's way of receiving confirmation from Cognito without the user handing over their password directly.&lt;/p&gt;

&lt;p&gt;Once I understood that distinction, I knew what to fix: create a separate Cognito app client configured for &lt;code&gt;authorization_code&lt;/code&gt; flow with the correct callback URL.&lt;/p&gt;

&lt;p&gt;There was a second, subtler issue. When the AgentCore Gateway's resource metadata (&lt;code&gt;/.well-known/oauth-protected-resource&lt;/code&gt;) doesn't specify a scope, Claude falls back to Cognito's OIDC discovery endpoint (&lt;code&gt;/.well-known/openid-configuration&lt;/code&gt;), which advertises standard scopes: &lt;code&gt;openid&lt;/code&gt;, &lt;code&gt;email&lt;/code&gt;, &lt;code&gt;phone&lt;/code&gt;, &lt;code&gt;profile&lt;/code&gt;. But my Cognito app client only allowed &lt;code&gt;openid&lt;/code&gt;, &lt;code&gt;smarthome-gateway/read&lt;/code&gt;, and &lt;code&gt;smarthome-gateway/write&lt;/code&gt;. Claude requesting &lt;code&gt;email&lt;/code&gt; and &lt;code&gt;phone&lt;/code&gt; caused an &lt;code&gt;invalid_scope&lt;/code&gt; error. The fix: explicitly configure the allowed scopes on the client to match exactly what Claude will request.&lt;/p&gt;

&lt;p&gt;Neither of these issues had anything to do with MCP itself. They were pure OAuth configuration problems. But you can only diagnose them if you understand the handshake well enough to know which step is failing.&lt;/p&gt;




&lt;h2&gt;
  
  
  Real-World Performance
&lt;/h2&gt;

&lt;p&gt;Once everything was wired up, I measured actual round-trip times from Claude's perspective:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Round-trip&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;get_status&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1,131 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;turn_on&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;3,367 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;get_status&lt;/code&gt; only hits the IoT Device Shadow — no trip to the physical bulb. &lt;/li&gt;
&lt;li&gt;
&lt;code&gt;turn_on&lt;/code&gt; goes the full path: Lambda → IoT Core → bridge → bulb → confirmation back. Three seconds is noticeable but acceptable for a chat experience with the light switch. For anything latency-sensitive, you'd want to think harder about this.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The monthly cost for 1,000 tool calls across all services: &lt;strong&gt;$0.07&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;A few things I want to clean up:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Terraform&lt;/strong&gt; for infrastructure provisioning. Right now I have five boto3 scripts that need to be run in a specific order. It works, but it's tedious. Terraform would make this reproducible and shareable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dockerize the local bridge&lt;/strong&gt; to run on a Raspberry Pi or similar edge device, so it doesn't depend on a laptop being on.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;More device types.&lt;/strong&gt; The architecture already supports it — &lt;code&gt;BaseDevice&lt;/code&gt; is an abstract interface, and the &lt;code&gt;DeviceRegistry&lt;/code&gt; manages multiple devices. Adding a smart plug or thermostat is mostly a new implementation, not a new architecture.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Closing Thoughts
&lt;/h2&gt;

&lt;p&gt;I started this project to understand MCP better. I ended up learning more about AWS IoT, OAuth flows, and serverless architecture than I expected. That's usually the sign of a good learning project — the stated goal was an excuse to dig into something real.&lt;/p&gt;

&lt;p&gt;A few things I'd tell someone starting this from scratch:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Get the local FastMCP version working first&lt;/strong&gt;. It takes an afternoon and gives you immediate feedback. Only then add the AWS layer.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The OAuth debugging will take longer than the infrastructure&lt;/strong&gt;. Learn the &lt;code&gt;authorization_code&lt;/code&gt; vs &lt;code&gt;client_credentials&lt;/code&gt; distinction before you start configuring Cognito — it'll save you hours.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;AgentCore Gateway is genuinely easy to set up compared to what I expected&lt;/strong&gt;. The tool schema verbosity is real, but it's a one-time cost.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;IoT Core is the right tool for this specific problem&lt;/strong&gt;. The NAT traversal alone justifies it.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;If you're building something similar or have questions about any of the architectural decisions, I'd love to hear from you in the comments. And if you spot something I got wrong — even better.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;→ &lt;a href="https://github.com/jui-hung-yuan/smarthome-mcp-lab" rel="noopener noreferrer"&gt;Full code on GitHub&lt;/a&gt;&lt;/p&gt;

</description>
      <category>aws</category>
      <category>mcp</category>
      <category>sideprojects</category>
      <category>ai</category>
    </item>
  </channel>
</rss>
