for Startups Tech Blog

フォースタ社員のエンジニアたちが思い思いのことを書き綴ります。

AI時代のモノづくりに。EMがGeminiと挑むHCD基礎検定とその理由

目次


はじめに

こんにちは。フォースタートアップスでエンジニアリングマネージャー(EM)をしている八巻(@hachimaki37)です。 現在、EMとして新たに取り組んだ自己研鑽があります。 それは「UI/UX領域の探求」と、「AIを活用して取り組む新しい学習法」です。 その結果として、2026年2月に実施された「HCD(人間中心設計)基礎検定」を受験し、45/50点という得点で無事に合格することができました。 HCD(人間中心設計)基礎検定は、過去問がほとんど存在しない試験において、私がどのようにGeminiと挑み、学習を進めたのか。そして、圧倒的なデリバリー速度でシステムを量産できるAI時代において、なぜ一人のEMが今「人に向き合うこと」を学ぶ必要があったのかについて、振り返ってみたいと思います。

以下、要点まとめです。

Geminiと会話しながら進める学習の「HOW」

未経験からの挑戦と公式テキストの壁

前提として、私はUI/UX領域については全くの未経験です。HCD基礎検定(HCD検®)の勉強を始める際、多くの受験者が直面する壁があります。それは「過去問が非公開であり、圧倒的にアウトプットの場が足りない」という点です。

公式HP(一般社団法人 人間中心社会共創機構)に掲載されている問題例はわずか3問。受験開始日の2週間前になると事前学習システムが解放され、5本の学習動画と5問の練習問題にアクセスできるようになりますが、当然これだけで広大な検定範囲をカバーすることはできません。

学習を進めていく中で強く感じたのは、「公式テキストのインプットだけでは物足りない」ということでした。公式テキストは非常に網羅的で素晴らしい内容ですが、それをひたすら読み込むだけでは知識が定着しているのか測れず、いざ実務に当てはめようとすると専門用語の定義が曖昧であることに気がつきました。

学習時間がタイトな中で、いかに知識習得と学習効率を高め、どうアウトプットとして模擬テストを実施していくかというアプローチを考えることにしました。それが、LLMを壁打ち相手にし、アウトプットの場を創り出すという戦略です。

なお、今回の学習では、より精度の高い問題生成と、過去の学習履歴という大量のコンテキストを正確に処理・分析させるために、Google AI Proを利用しています。また、Web検索で取得した情報もプロンプトのコンテキストに追加し、公式テキストにはない周辺知識の補完も行いました。たとえば、「ISO規格の定義」をより具体的な事例と紐づけて解説してもらい、その情報を自分のメモに追加するといった具合です。

この壁打ち学習法を活用し、2週間程度の学習期間で挑みました。 具体的な学習スケジュールは以下のように進めました。

期間 学習時間 学習内容・工夫
1周目 平日1.5h
休日1~2h
公式テキストと動画のインプットに加え、Geminiを使ってWebから必要な情報を取得し、プロンプトに入れるためのテキストデータ(要点まとめやメモ)を整理しました。ここでの工夫として、テキストデータがそれなりの分量になるため、手でのタイピングではなく、音声入力を積極的に使いGeminiと会話しながらコンテキストを揃え、文章を整えていきました。これにより、入力の手間を大幅に削減できました。
2周目 平日1h
休日1~2h
1周目で整理したテキストデータをGeminiに読み込ませ、ひたすら模擬試験を実施。
残りの期間 1日 30分程度 1日30分程度の学習時間で、模擬試験の結果をもとに、以下で紹介する「弱点克服」を徹底的に実践。

Geminiを利用した学習ループ

Geminiとの学習は、以下の3つのサイクルを試験当日までひたすら繰り返しながら進めました。

1. 自分の学習メモを読み込ませた模擬試験の生成

まず、Geminiに対して出題範囲と難易度を指定し、本番に近い4択クイズを作ってもらいます。単に「問題を作って」と指示するのではなく、自分の現在の学習進捗や、学習で整理したコンテキストをデータとして読み込ませた上で出題させました。

ちなみに、整理したテキストデータとは、公式テキストや動画を見ながら以下のようにまとめた、ごく簡単な箇条書きのメモです。これらを音声入力+Geminiと会話しながらコンテキストを揃え、文章を整えていきました。

【実際のプロンプト例】

あなたはHCD(人間中心設計)の専門家であり、優秀なテスト作成者です。 以下の私の現在の学習データ(整理したメモ)をコンテキストとして読み込み、私の理解度に合わせて応用レベルの4択クイズを10問作成してください。

人間中心のアプローチが広がっている背景

## 背景と理念

### ポイント
- ユーザーである人間中心に考えること
- ユーザーと一緒に、良い経験を共創すること
- 「モノ」と「コト」に対する考え方

### 人間中心デザインとは?
- モノ・コトに対して、「利用者視点」と「共創」によって新しい価値を生み出すこと
- 「問題の設定(発見)」と「解決策の探究(創造)」を「繰り返す」こと
- 「メソッド(プロセス+手法)」と「マインドセット(心構え・捉え方)」のこと

つまり、ユーザーが関わること。技術中心ではなく、人間の欲求・要求を中心にとらえる考え方

クイズを解き進める中で、正解・不正解の履歴データがプロンプト上に蓄積されていきます。単に「クイズを作って」とお願いすると毎回ランダムな出題になってしまいますが、この「自分が過去にどんな問題を解いて、どの選択肢で間違えたのか」というデータも一緒に渡すことで、Geminiは「自分の弱点がどこにあるのか」を分析してくれます。

これにより、単なるランダム出題のbotではなく、自分の苦手を把握した仕組みができました。

2. 解答結果からの弱点分析

クイズを解き終わったら、自分の解答結果(どの問題を正解し、どれを間違えたか)を再びGeminiに投げ込みます。そして、「私の解答結果を分析し、強みと弱点をフィードバックして」と指示します。

当初、AIから専用の学習ガイドや重要用語のフラッシュカードを作成する提案もありましたが、学習時間がタイトだったため私は使いませんでした。結果的には、学習コンテキストを読み込ませた「模擬試験の生成」と、次に紹介する間違えた項目に絞った「弱点特化型クイズ」だけで事足りました。

3. 弱点特化型クイズでの反復

弱点分析の結果をもとに、「前回間違えた問題の類題や、私が混同しやすい専門用語(例:デジタイゼーションとデジタライゼーションの違いなど)に特化した、難易度の高いクイズを5問作成して」と指示します。

これを全問正解できるまで繰り返すことで、知識の抜け漏れを塞ぐことができました。

学びと感想

実際の試験を終えてみての感想ですが、公式テキスト以外の内容も本番試験には多々出題されていたように感じました。そのため、自分で用意した学習コンテキストだけでは、正直なところ少々物足りなかった部分はあります。

しかし、それでもGeminiを使ったアウトプットの機会は非常に効率的で、やってよかったと感じています。

自分の理解度を可視化し、適切なタイミングで適切な難易度の問いを投げかけ、つまずいた時には解説してくれる。タイトなスケジュールの中でしたが、学習効率が大きく向上したと考えています。

なぜ、AI時代のEMに「HCD」が必要だったのか?

さて、学習の「HOW」についてはお伝えしましたが、そもそもなぜ、EMの私が、いまこのタイミングでUI/UX領域の「HCD」を学ぼうと思ったのか。

直接のきっかけは、こちらの記事(AI時代に本当に価値ある開発をするために - 人間中心設計で顧客が嬉しいものづくりを加速する)を読んだことでした。この記事から強いインスピレーションを受け、自分もこの領域に踏み込んでみたいと考えました。

しかし、その根底には、現代のソフトウェア開発を取り巻く環境への「ある強烈な危機感と気づき」がありました。

危機感と気づき

生成AIの進化により、エンジニアリングの世界は激変しました。AIを中心とした開発ワークフローの導入により、私たちのデリバリー速度は劇的に向上し、より少ないリソースで、より多くのサービス(モノ)を高速に量産できるようになりました。つまり、私たちは今、「モノをたくさん作る機会」をこれまで以上に手に入れたということだと思います。

一方で、私たちは、なぜこれほどたくさんの「モノ」を作るのだろうか? 何を目的に、誰のためにシステムを創るのだろうか?という疑念を抱くようになりました。AIの力でコードを量産し、高速にデプロイできたとしても、「そこにいるユーザーにとっての価値」がなければ、それは単なるガラクタの量産に過ぎません。

本当の価値とは、きっと誰も気がついていないような、埋もれた「小さな光」を見つけ出し、そこから少しずつ育んでいくものだと私は思っています。AI時代において「モノ」を創る能力がコモディティ化する中、最後に残る人間の真の価値は、モノの向こう側にいる「人」の感情や文脈を想像し、システムを通じてどのような「コト(体験)」を提供するのかに向き合うことこそが、誰の目にも触れていない「小さな光」に気づく方法なのではないかと思いました。

だからこそ、「人」を中心に設計する思考法、すなわちHCDを学ぶ必要があると感じ、今回の受験に至りました。

さいごに

HCD基礎検定資格を取得し、顧客をもっと理解するための活動が少し見え始めた気がします。また、HCDを学んだことで、日々の開発においてPdMやデザイナーが「なぜその体験にこだわるのか」の解像度が少し上がったように感じています。

もちろん、資格を取っただけで達人になれるわけではありません。しかし、ユーザーの声にならない声に耳を傾け、まだ誰も気づいていない「小さな光」を探し出すための「入り口」には、少し立つことができたように感じます。

私自身も、EMとしてまた違った新しいバリューを発揮し、技術と人の両面から、社会を少しでも前進させるような価値づくりに挑んでいきたいと思います。

ここまで長文にお付き合いいただき、ありがとうございました。 もしこの記事が、新しい学びへ挑戦しようとしている方や、AI時代のモノづくりに悩む誰かの「小さな光」になれば幸いです。

【初学者向け】MCPサーバー入門:まずは「ちょっと分かる」状態を目指す

テックブログのアイキャッチ画像

はじめに

こんにちは。フォースタートアップス株式会社エンジニアの田畑です。

MCPサーバーは2024年11月に発表されてから約1年半が経ち、技術トレンドの移り変わりが速い昨今では「今さら感」を感じる方もいるかもしれません。 ただその分、情報もある程度出揃っており、これから学ぶにはちょうどよい題材・タイミングだと思いました。

これからもAIと上手く付き合っていくための第一歩として、本記事ではMCPを題材に、チュートリアルレベルではありますが実際に手を動かしながら、「ちょっと分かる」状態を目指していきます。

MCPとは?

簡単にいうと、AIと外部のデータを連携するための仕組みを標準化したものです。 PCと外部データを接続するUSBのように、誰でも同じように扱える「規格」と捉えるとイメージしやすいです。

参考: What is the Model Context Protocol (MCP)?

MCPの全体構成

MCPは、主に「MCPホスト」「MCPクライアント」「MCPサーバー」の3つの要素から構成されています。 これらが連携することで、AIが外部のデータやツールをシームレスに利用できるようになります。

MCPの全体構成図
MCPの全体構成図

MCPホスト(Host)

ホストは、AIモデルを実行し、ユーザーと直接やり取りをするアプリ本体です。

例:Claude Desktop、Cursor、Visual Studio Codeなど

MCPクライアント(Client)

クライアントはホストの内部に組み込まれており、サーバーと通信を行うための「窓口」となる機能です。 ホストからの指示を受け取り、サーバーへリクエストを送ったり、サーバーからのレスポンスをホストへ返したりする役割を担います。

MCPサーバー(Server)

外部のデータソース(APIやデータベースなど)と直接つながり、MCPのルールに従ってAIにデータを提供する「橋渡し役」のプログラムです。

MCPという規格を利用することで、特定の機能やデータをAIから利用できるようになります。 たとえば、MCPサーバーを用意することで、AIが外部のAPIやデータベースにアクセスし、その結果をもとに回答できるようになります。

有名なものとしては、Slack MCP Server や GitHub MCP Server などがあります。

そしてこちらが、今回のメインテーマになります。

参考:

MCPの登場背景

そもそもMCPサーバーとは、何を目的として、何を解決するために登場したのでしょうか?以下は公式ドキュメントより抜粋した内容です。

最も高度なモデルでさえ、データとの連携が限られているという制約を抱えている。情報サイロやレガシーシステムに閉じ込められているため、新たなデータソースごとに独自のカスタム実装が必要となり、真に接続されたシステムの拡張は困難になっている。

MCPはこの課題に対処します。AIシステムとデータソースを接続するための普遍的でオープンな標準を提供し、断片化された統合を単一のプロトコルに置き換えます。その結果、AIシステムが必要なデータにアクセスするための、よりシンプルで信頼性の高い方法が実現します。

Introducing the Model Context Protocol

つまり、従来はAIが外部データと連携するたびに個別実装が必要で、拡張しづらいという課題がありました。 それを解決するために、AIとデータソースをつなぐ共通の仕組みとしてMCPが登場したようです。

MCPサーバーの構成

MCPサーバーは、主に「Tools」「Resources」「Prompts」の3つの機能から構成されます。

機能名 役割(ざっくり言うと?) 具体例
Tools(以下、ツール) AIが外部に対してアクションを実行するための機能(メイン機能) ・外部APIを呼び出して最新情報を取得する
・計算やデータ処理を行う
・データベースに情報を書き込む
Resources(以下、リソース) AIに読ませる「読み取り専用のデータ」(ユーザーが添付できる仮想ファイル) ・PC内のログファイル(error.log など)
・データベースのテーブル構造(スキーマ)
・システム全体を解説した仕様書テキスト
Prompts(以下、プロンプト) よく使う指示をまとめた呼び出し可能なテンプレート ・「議事録を要約して」テンプレート
・「バグ報告を整理して」テンプレート
・「この内容をブログ記事にして」テンプレート

MCPサーバーの内部構成と処理フロー
MCPサーバーの内部構成と処理フロー

図の処理の流れを順に追うと、以下の5つのステップになります。

  1. ユーザーの入力と準備

    ユーザーがAI(LLM)に対してチャットで質問や指示を行います。 このとき、必要に応じてMCPサーバーが提供する「指示テンプレート(プロンプト)」をユーザーが選択して指示を整形したり、「参考情報(リソース)」を付与して、AIに前提知識を与えることができます。

  2. AIの判断(情報が足りるか?)

    AIは、受け取った指示とリソースをもとに「この情報だけで回答できるか?」を判断します。情報が十分であれば、そのままツールを使わずに回答します。 一方で、ドキュメント検索など追加の情報が必要と判断した場合、次のステップに進みます。

  3. MCPサーバーへのツール実行リクエスト

    AIは、必要な情報を取得するために、MCPサーバーが提供する「ツール」を呼び出します(関数呼び出し)。

  4. 外部リソースへのアクセスとデータ取得

    呼び出されたツールは、必要に応じて外部リソース(ドキュメントやAPIなど)にアクセスし、情報を取得します。

  5. 回答の生成と出力

    取得したデータはMCPサーバーを通じてAIに返却されます。 AIは、元の質問・リソース・ツールの実行結果を統合し、最終的な回答を生成してユーザーに返します。

実際にMCPサーバーを作ってみる

一通り仕組みを理解したところで、実際にチュートリアルに沿ってMCPサーバーを作成します。

今回は、Railsにおけるクエリに関するドキュメントをデータソースとしたMCPサーバーを実装します。

実装の前提

なお、初期化や細かいセットアップについては本記事では扱いません。
詳細は公式チュートリアルをご参照ください。

動作確認環境

作成したMCPサーバーは、Claude Code Desktopをホストとして動作確認しています。

注意点
  • 対象サイトの利用規約やrobots.txtを事前に確認し、スクレイピングの可否を確認してください
  • 短時間に大量のリクエストを送らないようにし、適切な間隔でアクセスしてください

参考:公式チュートリアル

まずは全体像を把握するために、実装コード全体を掲載します。

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import * as cheerio from "cheerio";

const USER_AGENT = "rails-activerecord-mcp/1.0.0";
const QUERY_DOCS_URL = "https://guides.rubyonrails.org/active_record_querying.html";
const INTERNAL_RESOURCE_URI = "rails-activerecord://internal/guidelines";

const server = new McpServer({
  name: "rails-activerecord",
  version: "1.0.0",
});

const queryKeywordSchema = z.object({
  keyword: z.string().describe("クエリ設計に関する検索キーワード。例: N+1, includes, joins"),
});

const queryPromptArgsSchema = {
  userQuestion: z.string().describe("ユーザーから受け取った質問文"),
};

function buildInternalResourceText() {
  return `【社内Railsクエリ・ガイドライン】

  このドキュメントは、株式会社〇〇の社内ローカルルールです。
  公式ドキュメントよりも、この社内ルールを最優先して回答してください。

  【社内基本方針】
  - N+1問題の解消時、当社のプロジェクトでは原則として \`includes\` ではなく \`eager_load\` を第一候補とすること。(※JOINを強制してクエリ数を1つに抑えるため)
  - レコードの存在確認には \`present?\` ではなく、必ず \`exists?\` を使用すること。

  ※これら以外の一般的な仕様やメソッドの詳細について聞かれた場合は、適宜 \`get_query_docs\` ツールを使用して公式ドキュメントを検索してください。`;
}

server.registerResource(
  "internal_query_guidelines",
  INTERNAL_RESOURCE_URI,
  {
    title: "Internal Query Guidelines",
    description: "社内独自のRailsクエリ設計ガイドライン",
    mimeType: "text/plain",
  },
  async () => ({
    contents: [
      {
        uri: INTERNAL_RESOURCE_URI,
        mimeType: "text/plain",
        text: buildInternalResourceText(),
      },
    ],
  })
);

server.registerTool(
  "get_query_docs",
  {
    description: "ActiveRecordのクエリ設計に関する公式ドキュメントをキーワードで検索する",
    inputSchema: queryKeywordSchema,
  },
  async ({ keyword }) => {
    const headers = {
      "User-Agent": USER_AGENT,
      "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
    };

    try {
      const response = await fetch(QUERY_DOCS_URL, { headers });
      if (!response.ok) throw new Error(`Failed to fetch ${QUERY_DOCS_URL}: ${response.statusText}`);

      const rawHtml = await response.text();
      const doc = cheerio.load(rawHtml);
      const normalizedKeyword = keyword.toLowerCase();
      const sections: string[] = [];

      doc("h3, h4").each((_: number, el: any) => {
        const headingElement = doc(el);
        const heading = headingElement.text();
        const headingId = headingElement.attr("id") ?? "";
        const headingAnchorHref = headingElement.find("a").attr("href") ?? "";
        const headingAnchorText = headingElement.find("a").text();

        let content = "";
        let next = headingElement.next();
        while (next.length && !next.is("h3, h4")) {
          content += next.text() + " ";
          next = next.next();
        }

        const searchableHeadingValues = [heading, headingId, headingAnchorHref, headingAnchorText].join(" ").toLowerCase();
        if (searchableHeadingValues.includes(normalizedKeyword) || content.toLowerCase().includes(normalizedKeyword)) {
          sections.push(`${heading}\n${content}`);
        }
      });

      const result = sections.length > 0 ? sections.join("\n\n") : "該当する項目が見つかりませんでした";
      return { content: [{ type: "text" as const, text: result }] };
    } catch (error) {
      console.error(error);
      return { content: [{ type: "text" as const, text: "Failed to fetch documentation" }] };
    }
  }
);

server.registerPrompt(
  "active_record_assistant",
  {
    title: "Active Record Assistant",
    description: "ActiveRecordに関する質問に答えるための基本プロンプト",
    argsSchema: queryPromptArgsSchema,
  },
  async ({ userQuestion }) => ({
    messages: [
      {
        role: "user",
        content: {
          type: "text",
          text: [
            "あなたは Rails / ActiveRecord のサポートアシスタントです。",
            "まず internal_query_guidelines Resource を読み、社内ルールの有無を確認してください。",
            "質問が Resource の内容だけで答えられる場合は、Tool を使わずに回答してください。",
            "質問が一般仕様や詳細なメソッド挙動を必要とする場合のみ get_query_docs を使ってください。",
            "回答は次の形式で出力してください。",
            "1. 結論",
            "2. 根拠",
            "3. 使用した情報源: Resourceのみ / ResourceとTool",
            "4. 社内ルールを参照した場合は、その該当箇所を1行で明記",
            "",
            `ユーザーの質問: ${userQuestion}`,
          ].join("\n"),
        },
      },
    ],
  })
);

async function main() {
  const transport = new StdioServerTransport();
  await server.connect(transport);
}

main().catch((error) => {
  console.error("Fatal error in main():", error);
  process.exit(1);
});

上記の実装を、構成の観点からざっくり整理すると以下のようになります。

new McpServer()           // サーバー設定
server.registerResource() // リソース
server.registerTool()     // ツール
server.registerPrompt()   // プロンプト
server.connect(transport) // 起動処理

MCPサーバーの実装はシンプルで、サーバーを初期化し、リソース・ツール・プロンプトを登録したうえで起動する、という流れになっています。

1. サーバー設定
const USER_AGENT = "rails-activerecord-mcp/1.0.0";
const QUERY_DOCS_URL = "https://guides.rubyonrails.org/active_record_querying.html";
const INTERNAL_RESOURCE_URI = "rails-activerecord://internal/guidelines";

const server = new McpServer({
  name: "rails-activerecord",
  version: "1.0.0",
});

const queryKeywordSchema = z.object({
  keyword: z.string().describe("クエリ設計に関する検索キーワード。例: N+1, includes, joins"),
});

const queryPromptArgsSchema = {
  userQuestion: z.string().describe("ユーザーから受け取った質問文"),
};

const server = new McpServer({ ... }) でサーバーを初期化しています。nameversion で基本的な情報を設定しています。それ以外は、参照するURLやリクエスト設定、入力スキーマなどの定義です。

2. 起動処理
async function main() {
  const transport = new StdioServerTransport();
  await server.connect(transport);
}

main().catch((error) => {
  console.error("Fatal error in main():", error);
  process.exit(1);
});

サーバーの初期設定が完了したら、次にAIクライアントと通信し、サーバーを起動するための処理を記述します。(※ 実際のソースコードでは、この後に追加するリソースやツールの登録が完了した後、ファイルの最後に記述してください)

リソース・ツール・プロンプトの3つの機能を組み込んだMCPサーバーを起動し、AIクライアントと通信できる状態にします。 ここでは、トランスポート方式(通信方法)として StdioServerTransport() を指定しています。

MCPの通信方式は、大きく以下の2つに分けられます。

  • Streamable HTTP通信:外部サーバーにデプロイし、ネットワーク越しにやり取りする方式
  • stdio通信(標準入出力):ローカル環境で、AIクライアントと直接データをやり取りする方式

今回は、ローカル環境で手軽に動作させるために、stdio通信を利用しています。これにより、AIクライアントとMCPサーバーが直接やり取りできるようになります。

参考: Transports

3. リソース
function buildInternalResourceText() {
  return `【社内Railsクエリ・ガイドライン】

  このドキュメントは、株式会社〇〇の社内ローカルルールです。
  公式ドキュメントよりも、この社内ルールを最優先して回答してください。

  【社内基本方針】
  - N+1問題の解消時、当社のプロジェクトでは原則として \`includes\` ではなく \`eager_load\` を第一候補とすること。(※JOINを強制してクエリ数を1つに抑えるため)
  - レコードの存在確認には \`present?\` ではなく、必ず \`exists?\` を使用すること。

  ※これら以外の一般的な仕様やメソッドの詳細について聞かれた場合は、適宜 \`get_query_docs\` ツールを使用して公式ドキュメントを検索してください。`;
}

server.registerResource(
  "internal_query_guidelines",
  INTERNAL_RESOURCE_URI,
  {
    title: "Internal Query Guidelines",
    description: "社内独自のRailsクエリ設計ガイドライン",
    mimeType: "text/plain",
  },
  async () => ({
    contents: [
      {
        uri: INTERNAL_RESOURCE_URI,
        mimeType: "text/plain",
        text: buildInternalResourceText(),
      },
    ],
  })
);

リソースでは、LLMが参照するための静的な情報を定義します。今回はテキストをそのまま情報源として定義しましたが、テキストファイルや画像なども定義することが可能です。

テキストファイル・画像ファイルをリソースとして登録する場合は、それぞれ以下のように実装します。

import fs from "fs";

// テキストファイル(ログ)
server.registerResource(
  "system_error_log",
  "file:///logs/error.log",
  {
    title: "System Error Log",
    description: "システムのエラーログ",
    mimeType: "text/plain",
  },
  async () => ({
    contents: [
      {
        uri: "file:///logs/error.log",
        mimeType: "text/plain",
        text: fs.readFileSync("./logs/error.log", "utf-8"),
      },
    ],
  })
);

// 画像ファイル(バイナリ)
server.registerResource(
  "architecture_diagram",
  "file:///docs/architecture.png",
  {
    title: "Architecture Diagram",
    description: "システム構成図",
    mimeType: "image/png",
  },
  async () => ({
    contents: [
      {
        uri: "file:///docs/architecture.png",
        mimeType: "image/png",
        blob: fs.readFileSync("./docs/architecture.png").toString("base64"),
      },
    ],
  })
);

Claude Code Desktopの場合、追加したリソースは、チャット入力欄の「+(添付)」アイコンから「コネクタ」を経由して手動で呼び出すことができます。

リソースの適用手順

呼び出したリソースは、1つのテキストファイルとしてチャット欄に添付されます。さらに、そのファイルを開くと、コード内で定義した内容がそのまま含まれていることが確認できます。

リソースの適用手順

このようにClaude Code Desktopでは、リソースは自動的に参照されるものではなく、ユーザーが必要に応じてAIに渡す「前提知識のファイル」であることが分かります。

リソースの適用手順

そこで、実際にリソース(社内ガイドライン)を添付した状態で「N+1問題の解消方法」を質問してみました。その結果、AIはツールを使用せず、リソースの内容だけをもとに回答を生成しました。

リソースの適用手順

このように、AIは提供された情報だけで十分と判断した場合、ツールを実行せずに回答します。

次に、リソースには含まれていない pluck メソッドについて聞いてみました。

リソースの適用手順

するとAIは「手持ちの情報だけでは不十分」と判断し、get_query_docs ツールを実行して外部ドキュメントを参照したうえで回答を生成しました。

このように、AIはリソースとツールを状況に応じて使い分けながら回答を組み立てることがわかります。

補足

Claude Code Desktopでは、リソースを使用する際にユーザーが明示的に指定する必要があります。 一方で、他のAIホストでは、リソースの利用可否をクライアント側の実装で判断したり、状況に応じてAIが自動的に選択・利用するケースもあります。

参考: Resources

4. ツール
server.registerTool(
  "get_query_docs",
  {
    description: "ActiveRecordのクエリ設計に関する公式ドキュメントをキーワードで検索する",
    inputSchema: queryKeywordSchema,
  },
  async ({ keyword }) => {
    const headers = {
      "User-Agent": USER_AGENT,
      "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
    };

    try {
      const response = await fetch(QUERY_DOCS_URL, { headers });
      if (!response.ok) throw new Error(`Failed to fetch ${QUERY_DOCS_URL}: ${response.statusText}`);

      const rawHtml = await response.text();
      const doc = cheerio.load(rawHtml);
      const normalizedKeyword = keyword.toLowerCase();
      const sections: string[] = [];

      doc("h3, h4").each((_: number, el: any) => {
        const headingElement = doc(el);
        const heading = headingElement.text();
        const headingId = headingElement.attr("id") ?? "";
        const headingAnchorHref = headingElement.find("a").attr("href") ?? "";
        const headingAnchorText = headingElement.find("a").text();

        let content = "";
        let next = headingElement.next();
        while (next.length && !next.is("h3, h4")) {
          content += next.text() + " ";
          next = next.next();
        }

        const searchableHeadingValues = [heading, headingId, headingAnchorHref, headingAnchorText].join(" ").toLowerCase();
        if (searchableHeadingValues.includes(normalizedKeyword) || content.toLowerCase().includes(normalizedKeyword)) {
          sections.push(`${heading}\n${content}`);
        }
      });

      const result = sections.length > 0 ? sections.join("\n\n") : "該当する項目が見つかりませんでした";
      return { content: [{ type: "text" as const, text: result }] };
    } catch (error) {
      console.error(error);
      return { content: [{ type: "text" as const, text: "Failed to fetch documentation" }] };
    }
  }
);

まず全体の流れとしては、AIがユーザーの質問内容からキーワードを抽出し、そのキーワードをツールに渡します。ツールは受け取ったキーワードをもとに QUERY_DOCS_URL へリクエストを送り、該当する内容を取得して返す、という処理になっています。

このとき、コード上ではキーワード抽出の処理は実装していません。ツールの descriptioninputSchema をもとに、AIが質問文から適切なキーワードを判断し、自動的に引数として渡しています。

また、ページ全文をそのまま渡すと、不要な情報によってトークンを無駄に消費し、回答精度も下がってしまいます。そのため、h3h4 の見出し単位で内容を区切り、必要な部分のみを抽出してAIに渡しています。

ツールの実行手順

MCPサーバーあるあるかもしれませんが、LLMが賢すぎてツールを使わずにドヤ顔で自分の知識から答えてきました。特に一般的な質問では、MCPサーバーを介さずに完結してしまうケースもあります。

そのため、MCPサーバーを使って回答してほしい場合は、明示的に「ツールを使って」と指示したり、「公式ドキュメントから引用して」と逃げ道をなくしてあげるのがよさそうです。

MCPサーバーの利用を明示的に指示すると、ちゃんとツールを使って答えてくれました。

ツールの実行手順

5. プロンプト
server.registerPrompt(
  "active_record_assistant",
  {
    title: "Active Record Assistant",
    description: "ActiveRecordに関する質問に答えるための基本プロンプト",
    argsSchema: queryPromptArgsSchema,
  },
  async ({ userQuestion }) => ({
    messages: [
      {
        role: "user",
        content: {
          type: "text",
          text: [
            "あなたは Rails / ActiveRecord のサポートアシスタントです。",
            "まず internal_query_guidelines Resource を読み、社内ルールの有無を確認してください。",
            "質問が Resource の内容だけで答えられる場合は、Tool を使わずに回答してください。",
            "質問が一般仕様や詳細なメソッド挙動を必要とする場合のみ get_query_docs を使ってください。",
            "回答は次の形式で出力してください。",
            "1. 結論",
            "2. 根拠",
            "3. 使用した情報源: Resourceのみ / ResourceとTool",
            "4. 社内ルールを参照した場合は、その該当箇所を1行で明記",
            "",
            `ユーザーの質問: ${userQuestion}`,
          ].join("\n"),
        },
      },
    ],
  })
);

MCPサーバーにおけるプロンプトは、あらかじめ用意した指示書のようなものです。リソースと同様にチャット画面から選択して適用でき、AIの振る舞いや回答方針をあらかじめ定義することができます。

プロンプトの実行手順

プロンプトを選択すると、ユーザーの入力が求められます。

プロンプトの実行手順

質問を入力すると、入力した指示が含まれたプロンプト全体が、1つのテキストファイルとして自動で作成・添付されます。

プロンプトの実行手順

プロンプトの実行手順

冒頭にAIとのやり取りがありますが、それ以降はプロンプトで定義した通り、「結論 → 根拠 → 情報源 → 社内ルール参照の有無」の順で回答が生成されていることが確認できます。

一方で、N+1問題についてはリソース内でルールとして記載しているのですが、うまく読み取ってもらえなかったみたいです。このあたりは、表現やプロンプト設計を工夫することで改善の余地がありそうでした。

プロンプトの実行手順

プロンプトの実行手順

プロンプトの実行手順

終わりに

いかがでしたでしょうか。

本記事では、MCPサーバーの仕組みにフォーカスし、理解の解像度を上げることと、心理的なハードルを下げることを目的として、実際の実装例を交えながら紹介してきました。

本記事を通して、MCPサーバーについて「ちょっと分かる」状態になっていれば嬉しいです。

そのトレードオフ、消えるよ 🎾

目次

はじめに

こんにちは、フォースタートアップス株式会社エンジニアの野尻(@jsotakebmx)です。ヒューマンキャピタル支援システム(社内プロダクト)の開発を担当しています。
最初に断っておくと、この記事は完全に自戒です。 誰かの意思決定を指しているわけではなく、自分自身が「トレードオフ」という言葉をちゃんと使えているのか?を振り返った記録になります。

エンジニアをやっていると「トレードオフ」という言葉をよく使います。
技術選定、設計判断、スケジュールと品質のバランスをとるときなど…あらゆる場面で口にする機会があると思います。

実際私もそうです。意思決定の文脈でトレードオフという考え方をよく使いますし、ADR(Architecture Decision Record)を書くときも「トレードオフを踏まえた上で〜」という構成にすることが多いです。

ただ、最近「あれ、これって本当にトレードオフだったっけ?ただの妥協にトレードオフって名前をつけてないか?」と思い直すようになってきました。

この記事では、自分の経験を振り返りながら「トレードオフだと思っていたものが、本当にトレードオフだったのか?」を見つめ直してみたいと思います。

トレードオフが成立する条件

そもそも開発分野におけるトレードオフとは何かを改めて整理すると、私は「AとBの両方を最大化できないという構造的な制約の中で、意図的にどちらを優先するか選択すること」だと考えています。

個人的に考えるポイントは3つあります。

  1. 構造的な制約がある — AとBの両立が不可能である
  2. 意図的な選択がある — なんとなくではなく、判断している
  3. 犠牲の言語化ができる — 選ばなかった側の代償を説明できる

この3つが揃って初めてトレードオフと呼べるのではないかと思っている一方で、3つが揃っていたとしても永続するとは限らず、揃っているように見えて実は揃っていないこともあると思います。

改めて自分自身の経験を振り返ると、このトレードオフの成立根拠が消えてしまうパターンが3つありました。

賞味期限切れ

私のチームでは、「リリース速度を優先してテストの網羅性は犠牲にする」という意思決定がされてきました。フロントエンドのコアなビジネスロジック周りにはテストがあるものの、バックエンドにはほとんどありません。 この判断自体は、当時のコンテキストではトレードオフとして一定成立していたと思います。

限られたリソースの中で、テストを書く時間を実装に充てる。速度と品質保証の間で意図的に速度を選んでいる。構造的な制約、意図的な選択、犠牲の認識の3つの条件はある程度満たしていました。

ただ、ここで改めて「そのトレードオフ、今も成り立っているのか?」ということを問い直す必要があると感じています。
テストを書かないことで得られる「速度」は、本当に今も得られているのか。実感としては、むしろ逆のことが起きている気がしています。

テストがないことでリグレッションの検知が難しく、手動確認のコストも膨らみます。開発の後半になるほど保守性の悪化が効いてきて実装そのものが遅れたり、リリース後にバグが見つかれば、その対応で新規開発が止まります。
「テストを書かないことでスピードが上がる」はずだったのに、テストを書かないことでスピードが落ちている局面が出てきていると感じます。

さらに言えば、AI駆動の開発が当たり前になりつつある今、「テストを書く = 時間がかかる」という前提自体が一部揺らいでいるようにも感じています。テスト生成の補助が効く場面も増えてきた中で、「テストを書くコスト」と「テストを書かないコスト」のバランスは、判断した当時と同じなのか...

判断した瞬間は正しかった(あるいは少なくとも合理的だった)としても、前提が変われば結論も変わります。
一度下した判断を「確定したトレードオフ」として誰も見直さなければ、前提が変わっているのに結論だけが残る、「賞味期限切れ」の状態になってしまいます。

お腹を壊す前に、コンスタントに「賞味期限」を確認し新鮮な状態に戻してあげましょう。

情報の非対称

技術選定でADRを書くとき、複数の技術についてA vs Bの形式で整理することがあります。
実際私自身もADRを書くときは、トレードオフの体裁を取ることが多く、A/Bのメリット・デメリットを並べて、総合的に判断するように心がけています。

tech.forstartups.com

ただ、(当たり前かもしれませんが)形式だけ取っていてもあまり意味はありません。 たとえば、自分が知見のある技術Aについては5つのメリットと1つのデメリットが書いてあるのに、あまり触ったことのない技術Bについてはメリット1つ・デメリット2つ書かれているのでは、 情報量に明らかな差があります。

この状態で「比較した結果、Aを選びました」と言っても、それは結論が先にあって比較はそれを正当化するための装置になっている可能性があると思います。 トレードオフのフォーマットを借りた一種の確証バイアスとも言えるかもしれません。

誤解のないように書いておくと、知見のある技術を選ぶこと自体はかなり合理的だと思います。チーム/個人の習熟度は十分な判断軸ですし、未知の技術に賭けるリスクを避けることも妥当な選択です。 問題は、その構造を正直に書いているかどうかに尽きると考えています。
「技術的にAが優れているから選んだ」と書くのと、「Aの方がチームに知見があり、Bを十分に評価するコストを今は払えないからAを選んだ」と書くのでは、同じ結論でも誠実さが全然違いますよね。

後者は誠実な判断ですが、前者は(意図せずとも)妥協を隠してしまっていると言えるかもしれません。

制約の見落とし

そもそもトレードオフの比較対象にならないものを、比較対象として扱ってしまうケースです。

たとえば、レコメンド機能の開発を考えてみます。「ルールベースのレコメンドにするか、個人情報をAI解析してパーソナライズするか」という比較があったとします。レコメンド精度やユーザー体験を軸に比較すれば、AI解析の方が優れているように見えるかもしれません。

しかし、サービス規約で「個人情報を二次利用しません」と規定しているのであれば、AI解析という選択肢は最初から取れません。精度とコストの「トレードオフ」に見えていたものは、制約を見落としたまま存在しない選択肢を比較していただけです。

(もし、AI解析に興味があっても)技術的な興味はグッと堪えて、「その選択肢はビジネス上の制約としてそもそも選べるのか?」を最初に確認する必要があるなぁと感じています。

「消えるよ」🎾

3つのパターンを並べてみると、共通点が見えてきます。

パターン 実態 問題
賞味期限切れ 前提が変わったのに判断が残っている 再検討の欠如
情報の非対称 結論ありきの比較 フェアな評価の欠如
制約の見落とし 要件を満たさない案の採択 ビジネス要件との不整合

いずれも、ちゃんと見つめ直すとトレードオフとして成立していない(あるいは成立しなくなっている)ことに気づきます。前提が変わっていたり、比較がフェアでなかったり、そもそも選択肢じゃなかったり...

ただ、根拠が消えること自体は悪いことではなく、むしろ健全です。問題なのは、消えるべき根拠が消えずに残り続けて、意思決定の拠り所として引用され続けることだと思います。

自戒としてのチェックリスト

最後に、自分が意思決定をするときに確認したいことをまとめておきます。

「これはトレードオフか?」を判定する3つの問い

  • その判断の「前提」は、今も変わっていないか?
    • 前提が変わっているなら、結論だけ残っていても意味がない。トレードオフには賞味期限がある。
  • 選ばなかった側を、同じ熱量で説明できるか?
    • できないなら、情報が足りていないか、結論ありきになっている可能性がある。
  • 比較している軸は、達成すべきゴールに紐づいているか?
    • ビジネス要件と関係ない観点で優劣をつけていないか。技術的な面白さとビジネス上の正しさは別物。

まとめ

「トレードオフ」は便利な言葉です。使った瞬間に意思決定が合理的に見えるし、犠牲を伴っている感じが出るので説得力もあります。だからこそ、妥協や前提の変化を覆い隠す言葉としても機能してしまいがちです。

自分がこれまでトレードオフだと思っていたものの中に、実は賞味期限が切れていたり、比較がフェアでなかったり、そもそも成立していなかったものが混ざっていなかったか... 正直なところ、ゼロではないと思います。

「うーん、これはトレードオフ! 😀」と言ってしまいそうな時、その判断が本当にトレードオフとして成立しているのか、一拍置いて考える。そんな自戒でした。

デザイナーが入社して4ヶ月で、業務を理解し課題を見つけるために行ったこと

目次

はじめに

はじめまして。フォースタートアップス株式会社のUI/UXデザイナーいのうです。
ヒューマンキャピタル支援システム(社内プロダクト)を担当しています。
このシステムは、主にヒューマンキャピタリストが日々の業務で活用しており、支援業務に欠かせない基幹システムとなっています。

デザイナーとして新しい環境に飛び込んだとき、「事業会社のデザイナーは、入社後どのようにドメイン理解を深めているのだろう?」と疑問に思い、他社デザイナーのブログ記事を探した経験があります。

そこで今回は、私が入社後4ヶ月間で取り組んだ業務理解から課題発見、そして優先順位付けまでを、実体験を元に共有します。
私と同じように、新しい環境に飛び込み、何から手をつければいいか模索しているデザイナーの方々へ、この記事がヒントになれば幸いです。

プロダクトの現状把握と分析

1. 業務フローの可視化

社内資料の読み込みに加え、サービスブループリントを用いました。サービスブループリントでは、実務担当者・顧客・システムとの接点を時系列で可視化します。
実務上のより詳細なプロセスは残されているものの、まずは全体像を整理したことで、各ステークホルダー間の情報の流れや、プロダクトが介在するポイントを把握することができました。

サービスブループリントの画像
サービスブループリント

2. ウォークスルー & ヒューリスティック分析

ユーザー視点でメイン導線のウォークスルーを実施し、全画面のキャプチャを元にヒューリスティック原則に照らし、評価を行いました。
ヒューリスティック評価とは、経験則に基づいてユーザビリティを評価し、UI上の課題を発見する手法です。今回は、ニールセンの「10原則」を基準としました。
具体的にはキャプチャした各画面に対して、10原則のどの観点に該当するかを明記した上で、改善コメントを残していきました。
例えば、【Efficiency : 柔軟性と効率性】の観点から、課題や改善点を記載するといったように、原則とセットで気付きを言語化しました。

3. システム構造の理解

ナビゲーション構造や画面遷移図をFigJamで可視化し、システム構造の整理を進めました。
この過程で、既存設計の工夫されている点と、課題を抽出しました。

このようなFigJamのテンプレートをベースに実際の遷移図を作成しました。

4. ライティングの洗い出しと改善案の検討

プロダクト内のテキスト全体を調査し、表記揺れや改善の余地がある表現を洗い出しました。
これらをNotionに集約してプロジェクトチーム内で共有し、ユーザーにとってより分かりやすい表現や適切な表現についての議論を進めました。

ユーザー理解

1. ユーザーテストへの同席

実際にユーザーがプロダクトを操作しているところを間近で見ると、想定していなかった操作手順や、見落としていた視点など、多くの発見がありました。
その場で得られたフィードバックは、具体的な課題の抽出につながりました。実際に、現場の実態を直接自分の目で見たからこそ、得られた気づきであり、その重要性を改めて認識しました。

2. ユーザーインタビュー

実務で利用しているユーザーへのインタビューを実施しました。
設問設計から行い、実際の業務担当者ならではの視点や、作り手側では気づきにくいペインポイントを確認することができました。

課題の構造化と優先順位付け

1. 課題の抽出と分類

まず、自分自身の視点で気づいた課題やインタビューを行った中で抽出された課題をFigJamの付箋に集約しました。
次に、「ユーザーに与える影響度」と「施策にかかる時間」の2軸でマッピングを行いました。

縦軸にはUXピラミッドというフレームを元に、ユーザーに与える影響度を4段階に分けました。
横軸はギャレットの5段階モデルを元にスコープを分けています。

抽出した課題をギャレットの5段階モデルに分類したことで、自分が現時点でどのレイヤーを理解できているかが明確になりました。
表層(UI)については多く気づくことができましたが、上層の要件・戦略に関わる課題は業務についてのより深い理解が必要だと感じました。
この分類はrootさんの課題の分析と改善施策の発散プロセスを参考にしています。

課題の抽出と分類を行ったFigJamの画像
課題の抽出と分類

2. 課題の優先順位付けと実施方向性の検討

自分自身の気づき、要望、ユーザーフィードバックに基づいて抽出した課題をNotionのデータベースへ集約しました。
各課題に対して想定工数や優先度を定義し、プロジェクトチーム内で対応方針の議論を行っています。
これらの情報はNotionのプロパティで一元管理しており、プロジェクトの全体像の把握だけでなく、個別の課題における背景や経緯を記録するログとしても活用しています。

4ヶ月を通して得た気づきと今後の展望

この4ヶ月間を通じて、最適な情報設計は、業務やユーザーへの理解があってこそだと痛感しています。

また、分析結果をもとにマネージャーやPM、CTOへプレゼンを重ねたことで、意思決定層に向けた資料構成や伝え方のスキルを磨くことができました。
発見した課題をどのように改善提案へと繋げるか、一連のプロセスを経験できたことは、今後の糧となりました。

また、今後自分自身のドメイン知識が深まったとしても、「現場で業務にあたっているユーザーこそが最も詳しい」という事実を忘れず、常にユーザーの声に耳を傾ける姿勢を大切にしていきたいです。

現在、2026年度のロードマップに取り組んでいますが、さらなる情報の深掘りが必要な箇所が出てきました。
そのため、現在は設計の精度を高めるためのユーザーインタビューを計画中です。
今後もユーザーに寄り添い、ビジネスにおけるKPIの双方に寄与できる視点を持ち、プロダクト改善に取り組んでいきたいと考えています。

何から手をつければいいか悩むデザイナーの方へ

新しい環境で「何から手をつければ…」と悩んでいる方に、私自身の経験からお伝えしたいのは、まずは一人で完結できることからスタートすることです。

組織によってはUX文化が浸透していなかったり、ドキュメントが整備されていなかったりすることもありますが、まずは自分で動ける範囲から手をつけてみるのがおすすめです。

  • 社内情報の収集:社内ドキュメントやSlackログを読み込み、文脈を拾う
  • AIの活用:業界の全体像や専門用語をAIに整理してもらい、基礎知識を把握する
  • 外部リソースの調査: 業界レポートやカオスマップを探して読み、市場の全体像を掴む

まずは自分で把握できる範囲から整理し、カスタマーサクセスへのヒアリングや商談への同席など、段階的に周囲を巻き込む範囲を広げていくのが、理解を深めるための着実なステップになるはずです。