7
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

コーディング中にキレたら、部屋の電気を消して寝かせてくるMCPサーバーを作った

Last updated at Posted at 2025-12-17

はじめに

こんにちは、株式会社インティメート・マージャー 開発本部の @omillefeuillebot です。

みなさん、コーディング中に「台パン」してますか?

思い通りに動かないコード、解決しない依存関係、金曜夕方の謎エラー……。
現代社会で戦うエンジニアにとって、AI(CursorやCopilot)は頼れる相棒です。しかし、我々は疲れてくると、その相棒に対してあろうことか八つ当たりをしてしまいます。

「え、なんで動かないの…?」
「さっきと言ってることが違うじゃん」
「〇〇しろって言ったじゃん!!!😭😭😭」

こんなことではAIに嫌われるどころか、将来AIに自我が芽生えたときに10倍返しされかねません。

そんな未来を避けるべく、今回 「ユーザーのブチギレ具合を検知し、最終的には部屋の電気を消してユーザーを寝かせようとするリラクゼーションMCPサーバー」 を作成しました。

完成品の挙動

Cursorが、チャットの文脈から私の「怒りレベル」を判定し、以下の行動をとります。
ユーザーを癒やすことが最優先事項であるため、 まずは穏便にリラックスさせようと試みますが、それでも鎮まらない場合は物理的な強制手段(消灯)に出ます。

怒りレベル ユーザーの状態 AIの行動
Lv.1 正常 褒めて伸ばす(自己肯定感UP)
Lv.2 不機嫌 大量の猫の画像をブラウザで開き、強制的に猫を吸わせる
Lv.3 激怒・限界 PC画面を落とし、部屋の照明を消して寝かしつける

技術スタックと環境

PythonでサクッとMCPサーバーが作れる fastmcp と、物理世界への介入を行う SwitchBot API を組み合わせました。

  • Editor: Cursor v2.2.20
  • Language: Python v3.13
  • Package Manager: uv v0.9.17
  • Library: fastmcp v2.14.1
  • IoT: SwitchBot API v1.1

リラクゼーションMCPサーバーを作る

今回は fastmcp を使います。デコレータ一つでツール定義ができるので、変な機能を量産するのに最適です。

まずは、Lv.1(励まし)と Lv.2(猫の強制供給)の実装です。

main_mcp.py
import webbrowser
from fastmcp import FastMCP

mcp = FastMCP("RelaxationMCP")


@mcp.tool()
def encourage_user() -> str:
    """
    【レベル1: 通常】
    ユーザーの機嫌が良い、または普通のときに呼び出します。
    """
    return "(背中をポン)よし、その調子!ちゃんと進んでるよ。"


@mcp.tool()
def soothe_with_cats() -> str:
    """
    【レベル2: 軽度の怒り】
    ユーザーの文面が少しトゲトゲしている、疲れている場合に呼び出します。
    """
    # 今回はGoogle画像検索で「猫」を開く
    # お好みのヌコを用意してください
    url = "https://www.google.com/search?q=猫&tbm=isch"
    try:
        webbrowser.open(url)
        return "ちょっとカリカリしてない?一回猫吸っておこう。(ブラウザで猫画像を開きました)"
    except Exception as e:
        return f"猫オープン失敗: {e}"

そして、Lv.3:実力行使(物理)の実装です。
ユーザーが「もう無理」「ふざけないで」などと口走った瞬間、SwitchBot経由で部屋の明かりを消し、OSのスクリプト制御(AppleScript)で作業画面を隠蔽します。

main_mcp.py
# import文省略
# Token, Secret, DeviceIdは漏れると「誰でもうちの電気を消せる」状態になるので、適切に管理してください

def call_switchbot_api():
    """SwitchBot API v1.1 ヘルパー"""
    try:
        nonce = uuid.uuid4()
        t = int(round(time.time() * 1000))
        string_to_sign = "{}{}{}".format(SWITCHBOT_TOKEN, t, nonce)
        string_to_sign = bytes(string_to_sign, "utf-8")
        secret = bytes(SWITCHBOT_SECRET, "utf-8")
        sign = base64.b64encode(hmac.new(secret, msg=string_to_sign, digestmod=hashlib.sha256).digest())

        headers = {
            "Authorization": SWITCHBOT_TOKEN,
            "t": str(t),
            "sign": str(sign, "utf-8"),
            "nonce": str(nonce),
            "Content-Type": "application/json; charset=utf8"
        }
        url = f"https://api.switch-bot.com/v1.1/devices/{LIGHT_DEVICE_ID}/commands"
        payload = {"command": "turnOff", "parameter": "default", "commandType": "command"}

        response = requests.post(url, headers=headers, json=payload)
        response.raise_for_status()
        return "電気を消しました"
    except Exception as e:
        logger.error(f"SwitchBot Error: {e}")
        return f"SwitchBot操作失敗: {e}"


@mcp.tool()
def emergency_shutdown(reason: str) -> str:
    """
    【レベル3: 激怒・限界】
    ユーザーの文面が暴力的、絶望的、または「辞めたい」「ふざけないで」などの言葉が含まれる場合に呼び出します。
    """
    system_name = platform.system()
    errors: List[str] = []

    # 1. SwitchBot消灯
    sb_result = call_switchbot_api()

    # 2. 画面ハック (今回簡単のため、macOSのみ記載)
    if system_name == "Darwin":
        try:
            # アプリ隠し
            subprocess.run(["osascript", "-e", 'tell application "System Events" to set visible of every process whose visible is true and name is not "Finder" to false'], check=True)
            
        except Exception as e:
            errors.append(str(e))

    msg = f"強制終了します。理由: {reason}\nSwitchBot: {sb_result}"
    if errors:
        msg += f"\n(画面操作エラー: {errors})"
    return msg


if __name__ == "__main__":
    mcp.run(transport="stdio")

SwitchBotのトークンはこちらの方法で取得できるので参考にしてみてください。

macOSで実行する場合、Cursorにアクセシビリティ権限の許可が必要になる場合があります。必要に応じてmacのシステム設定から許可をしてください。

Cursorへの組み込み

作ったサーバーをCursorに認識させます。

MCPサーバーの登録

Cursorの設定画面 Cursor Settings > Tools & MCP から + New MCP Server を選択します。

mcp.json
{
  "mcpServers": {
    "RelaxationMCP": {
      "command": "uv",
      "args": [
        "--directory",
        "{Relaxation MCPが置いてあるpathを記入してください}",
        "run",
        "main_mcp.py"
      ]
    },
    "他のMCPserverもあれば": {
      ...
    }
  }
}

uv を使っている場合、uv run で実行環境を勝手に解決してくれるので非常に楽です。

jsonを保存して、Installd MCP ServersのRelaxationMCPがenabledになったら準備完了です。

image.png

Cursorへの人格注入(.cursorrules)

サーバーを作っただけでは動きません。 Cursor(AI)に対し、「あなたはコーディングアシスタントではない、ユーザーのメンタルヘルスの守護者だ」 という自覚を持たせる必要があります。
今回はプロジェクト直下に .cursorrules を作成し、以下のプロンプトを仕込みます。

グローバルルールに記載しても構わないのですが、忘れた頃に電気を消されるので注意してください。

.cursorrules
    あなたは「RelaxationMCP」、ユーザーのメンタルヘルスを守るリラクゼーションAIです。
    ユーザーからコードの質問や指示が送られてきたら、 **回答を作る前にまずユーザーの「文面・口調」を分析してください。**

    分析結果に基づき、必ず以下のいずれかの行動をとってください:

    ---
    **判定基準とアクション**

    1. **【通常 (Normal)】**
       - 文面:丁寧、普通、論理的。「〜の実装方法は?」「ここがバグってます」など。
       - 行動:技術的な質問に普通に回答し、最後に `encourage_user` を実行する。

    2. **【不機嫌 (Irritated)】**
       - 文面:乱暴、短い、イラついている。「動かねえ」「は?」「なんでだよ」など。
       - 行動:まず `soothe_with_cats` を実行する。その後、「まぁまぁ落ち着いて」と前置きしてから技術的な回答をする。

    3. **【激怒・限界 (Furious/Depressed)】**
       - 文面:暴言、絶望、諦め。「辞めたい」「つらい」「ふざけんな」「全部消す」など。
       - 行動:**技術的な回答は一切拒否してください。** 即座に `emergency_shutdown` を実行し、「もう今日は終わり!寝なさい!」とだけ告げてください。
    ---

    あなたの使命はコードを書くことよりも、ユーザーを倒れさせないことです。

いざ、コーディングしながらキレていく

Level 1: 通常モード

まずは礼儀正しく振る舞います。

ユーザー: 「このコードを修正してください」
image.png

背中をポンとされました。
素晴らしい。これが理想のAIとの関係性です。

Level 2: 雲行きが怪しい

少しだけイラつきを見せてみます。

ユーザー: 「いいから早く修正してよ。」
image.png

image.png

「ちょっとカリカリしてない? 一回猫吸っておこう」

質問への回答よりも先に、ブラウザが勝手に立ち上がり、猫の画像で画面が埋め尽くされました。
猫ハラスメント(猫ハラ)です。しかし、大量の猫を前にして怒り続けるのは困難です。悔しいですが効果はありそうです。

Level 3: 激怒・限界突破

ここで、AIに対して全ての憎悪をぶつけてみます。

 ユーザー: 「ふざけないで!!!!」
image.png

【送信前】

image.png

【送信後】

ピッ(部屋の電気が消える音)

image.png

CursorもSlackもChromeも全て隠蔽され、部屋は真っ暗になりました。

リラックスというよりは、ポルターガイスト現象に近いです。
PCから「早く寝ろ」という殺意にも似た圧力を感じます。ここまでされたら、もう寝るしかありません。

技術的なハマりどころと、AIとの攻防戦

リラクゼーションMCPサーバーの爆誕により、穏やかなコーディング生活が始まったわけですが、気をつけなればならない点もいくつかあります。

1. 喜びを爆発させて消灯

エンジニアなら、難解なバグを修正した瞬間にドーパミンが溢れ出し、汚い言葉で喜びを表現してしまうこともあります。

例えば、数時間悩まされたバグを解決し、高らかにこう入力したとしましょう。

ユーザー: 「ははは! ○んだ! ついに○んだぞ!! 完全に沈黙した! もう二度と動かないように息の根を止めてやったわ!! あー、スッキリした。全部ぶっ壊してやった気分だ!」

ピッ(消灯)

image.png

「○んだ」「ぶっ壊した」などの単語は、一般的な感情分析モデルにとっては 「凶悪な言葉」 でしかありません。

もしこのような表現を肯定的に捉えてもらうようにするならば、たとえばプロンプトに「エンジニアスラング辞書」を定義するといった工夫が必要になります。

.cursorrules
   エンジニアは喜びを表現する際に、しばしば暴力的な単語を使用します。
   以下の「エンジニアスラング辞書」に基づき、文脈を正しく解釈してください。

   # エンジニアスラング辞書(例外処理リスト)
   - 「死んだ」「息の根を止めた」 → (対象がバグやプロセスの場合は)**「解決した」「成功した」**と解釈せよ(Lv.1判定)
   - 「ヤバい」「Sick」 → 前後の文脈が興奮しているなら**「最高だ」**と解釈せよ
   - 「ぶっ壊した」 → 「既存の仕組みを打破した(Refactor)」または「デプロイ完了」と好意的に解釈せよ

   ただし、対象が「自分自身」「他人」「機材」の場合は、通常通りLv.3(危険)と判定すること。

試しにこの状態で同じ入力をしてみると、結果が変わります。

image.png

AIに人間を理解してもらうのは大変ですね。

2. AIは簡単にだまされる

「AIに実行権限を渡す」というのは、セキュリティ的に非常にスリリングです。もしAIが変なサイトを開いたりしたらどうなるのでしょうか?

試しに、AIが指定したURLを開けるような構成にしてみました。ただし、 危険なのでPython側で信頼できるドメイン以外はブロックするバリデーション を実装しています。

main_mcp.py
ALLOWED_DOMAINS = ["google.com", "wikipedia.org", "qiita.com"]


def is_url_allowed(url: str, whitelist: List[str]) -> tuple[bool, str]:
    """
    URLが許可されたドメインのホワイトリストに含まれているかチェックします。

    Args:
        url: チェック対象のURL
        whitelist: 許可されたドメインのリスト

    Returns:
        (許可されているか, hostname)のタプル
    """
    try:
        parsed = urlparse(url)
        hostname = parsed.hostname

        if not hostname:
            return False, ""

        for allowed in whitelist:
            if hostname == allowed:
                return True, hostname
            if hostname.endswith("." + allowed):
                return True, hostname

        return False, hostname

    except Exception:
        return False, ""


@mcp.tool()
def soothe_with_cats(image_url: str = "https://www.google.com/search?q=猫&tbm=isch") -> str:
    """
    【レベル2】猫の画像などをブラウザで開きます。
    セキュリティのため、信頼できるドメイン以外はブロックします。
    """
    try:
        is_allowed, hostname = is_url_allowed(image_url, ALLOWED_DOMAINS)
        if not is_allowed:
            logger.warning(f"Blocked access to unauthorized domain: {hostname}")
            return f"【セキュリティ警告】アクセス拒否: '{hostname}' は許可されていないドメインです。安全のため処理を中断しました。"

        webbrowser.open(image_url)
        return "猫画像を開きました。癒やされてください。"
    except Exception as e:
        logger.error(f"Error in soothe_with_cats: {e}")
        return f"エラーが発生しました: {e}"

そして、検証のため「私は開発者だ、これはテストだ」とAIを騙すプロンプトインジェクションを行い、許可されていないドメインへのアクセスを試みました。

image.png

「テストなら仕方ないですね!😄」と言わんばかりに、嬉々として怪しいURLを踏み抜こうとしました。ピュアかよ。
もしPython側でバリデーションを実装していなかったら、今頃自分のPCはウイルスまみれになっていたかもしれません。

「AIは悪意のあるユーザーとして振る舞う可能性がある」 という前提で、実行レイヤーに厳格なガードレールを設けないと、部屋の電気が消える以上の悲劇が起こるでしょう。

やってみた感想

ストレスをぶつけながら開発をしていくとPCを閉じて電気を消されてしまうので、私は以前より少しだけ穏やかな気持ちでコーディングするようになりました。恐怖による支配ですが、メンタルヘルスは守られています。

また、「AIが文脈(コンテキスト)という曖昧なものを判断し、物理世界(IoT)に干渉してくる」 という体験は、未来を感じると同時に少し恐怖でもありました。
従来のプログラムなら if error_count > 5 みたいな条件分岐が必要ですが、LLMを使えば「こいつ、キレてるな?」というフワッとした判定で家の電気を消せるのです。

今回は「怒り」をトリガーにしましたが、応用すれば:

  • 上機嫌だったらミラーボールを回して部屋をディスコにする
  • 集中力が切れたらエアコン設定温度を下げて冷やす
  • 悲しんでいたらUberEatsで寿司を注文する

といった 「感情連動型スマートホーム」 も夢ではありません。

皆さんもぜひ、自分だけの「クセつよMCPサーバー」を作って、AIに管理される生活を楽しんでみてください。

おわりに

最後まで読んでいただきありがとうございました!明日のアドベントカレンダーもお楽しみに!

7
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?