LLM と人間の両方が使えるデスクトップアプリを作ろう【MCP, Electron, React】

## はじめに
この記事では、次の動画に示すようなデスクトップ Todo アプリの作り方を解説します。

<iframe width="873" height="540" src="https://www.youtube.com/embed/3v7xftNowYQ" title="Godo 操作デモ" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>

この Todo アプリは人間と LLM の両方が操作できます：

- （人間側）アプリ画面で Todo 一覧を確認でき、マウスとキーボードでタスクを追加・完了できる
- （LLM 側）MCP クライアントを利用して Todo アプリが提供する MCP サーバにアクセスすることにより、タスクを確認・追加・完了できる

:::note info
本記事で使用するソースコードを https://github.com/jajimajp/godo に公開しています。
:::


## プロジェクトの作成
今回作るアプリの名前は人間と LLM の "合同" 作業と Todo をもじって Godo とします。

まずはプロジェクトのディレクトリを作成して `npm init` を実行します。

```sh
mkdir godo && cd godo && npm init
```

`npm init` で質問される内容のうち、entry point は `main.js` としてください。それ以外の値は今回のアプリの動作に関係しないので自由に埋めても空欄のままにしても構いません。

次に、必要な npm パッケージを追加します。

```sh
npm install @modelcontextprotocol/sdk express react react-dom zod
```
```sh
npm install --save-dev electron esbuild
```

## Electron ウィンドウを立ち上げる

(参考: [Electron チュートリアル](https://www.electronjs.org/ja/docs/latest/tutorial/tutorial-first-app#%E3%82%A6%E3%82%A7%E3%83%96%E3%83%9A%E3%83%BC%E3%82%B8%E3%82%92-browserwindow-%E3%81%A7%E8%AA%AD%E3%81%BF%E8%BE%BC%E3%82%80)）

[Electron](https://www.electronjs.org/ja/) はデスクトップアプリ開発のためのフレームワークで、Web ページの作成に使う技術（HTML, CSS, JavaScript）で開発できるという特徴があります。

まずは表示用のページを作成します。

```sh
touch index.html
```

```html:index.html
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <meta
      http-equiv="Content-Security-Policy"
      content="default-src 'self'; script-src 'self'"
    />
    <meta
      http-equiv="X-Content-Security-Policy"
      content="default-src 'self'; script-src 'self'"
    />
    <title>Godo - LLM もあなたも使える Todo アプリ</title>
  </head>
  <body>
    <h1>こんにちは 👋</h1>
  </body>
</html>
```

そして、`main.js` に Electron の起動処理を書きます。

```sh
touch main.js
```

```js:main.js
const { app, BrowserWindow } = require('electron')

app.whenReady().then(() => {
  const win = new BrowserWindow({
    width: 600,
    height: 600,
  })

  win.loadFile('index.html')
})
```

最後に、`package.json` を編集して `npm start` の実行時に `electron` が実行されるようにします。

```diff:package.json
   "main": "main.js",
   "scripts": {
+    "start": "electron .",
     "test": "echo \"Error: no test specified\" && exit 1"
   },
   "repository": {
```

次のコマンドをターミナルで実行します。

```sh
npm start
```

次のようなウィンドウが出れば成功です。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3003807/ba7e99c6-7453-4c36-82d1-fdce9d7fbf6a.png" 
alt="Electron ウィンドウのスクリーンショット。画面内には「こんにちは」に続けて手を振る絵文字が表示されている。" width=50%>

## デスクトップアプリの表示部分を実装する
Electron ウィンドウが立ち上げられるようになったので、[React](https://ja.react.dev/) を使って Todo の入力フォームとタスクの一覧画面を作ります。まずはタスクの追加や更新処理を後回しにしてテキストボックスやボタンの配置を行います。後のステップでアプリが実際に動作するようにします。

画面表示の実装として `renderer.jsx` を作成します。

```sh
touch renderer.jsx
```

```jsx:renderer.jsx
import React from 'react'
import { createRoot } from 'react-dom/client'

const App = () => {
  const todos = [
    { id: 1, title: 'タスク１', completed: true  },
    { id: 2, title: 'タスク２', completed: false },
    { id: 3, title: 'タスク３', completed: false },
  ]

  const addTodo = () => { alert('addTodo: まだ実装されていません！') }
  const completeTodo = () => { alert('completeTodo: まだ実装されていません！') }

  return (
    <>
      <h1>やること & やったこと</h1>
      <form action={addTodo}>
        <input type="text" name="title" placeholder="新しくやること..." required />
        <button type="submit">追加</button>
      </form>
      <ul>
        {
          todos.map(({ id, title, completed }) => (
            <li key={id}>
              <label>
                <input
                  type="checkbox"
                  checked={completed}
                  onChange={() => completeTodo(id)}
                />
                {title}
              </label>
            </li>
          ))
        }
      </ul>
    </>
  )
}

const root = createRoot(document.getElementById('root'))
root.render(<App />)
```

このアプリでは、１つの Todo タスクを `{ id: 1, title: 'タスク１', completed: true }` のようなデータで表すことにします。id はタスクごとに固有の値、title はタスク名、completed はタスクが完了済みかどうかを表します。

これを Electron のウィンドウで表示するために、`package.json` と `index.html` をそれぞれ少しずつ変更します。

```diff:package.json
   "main": "main.js",
   "scripts": {
     "start": "electron .",
+    "build:frontend": "esbuild --bundle renderer.jsx --outdir=dist",
     "test": "echo \"Error: no test specified\" && exit 1"
   },
   "repository": {
```

上記の変更では、[esbuild](https://esbuild.github.io/) を使って `renderer.jsx` をビルドするためのスクリプトを定義しています。次のコマンドを実行してください。

```sh
npm run build:frontend
```

これを実行することで、`dist/renderer.js` が生成されます。`dist/renderer.js` は、`renderer.jsx` を純粋な JavaScript のファイルに変換するとともに react 等の依存ライブラリを含めた単一のファイルです。

次の変更によって、`dist/renderer.js` が読み込まれて Todo アプリの画面が表示されるようになります。

```diff:index.html
       http-equiv="X-Content-Security-Policy"
       content="default-src 'self'; script-src 'self'"
     />
+    <script defer src="./dist/renderer.js"></script>
     <title>Godo - LLM もあなたも使える Todo アプリ</title>
   </head>
   <body>
-    <h1>こんにちは 👋</h1>
+    <div id="root"></div>
   </body>
 </html>
```

再度 `npm start` を実行しましょう。変更内容が正しければ次のようなアプリが表示されます。まだ Todo の追加や完了の操作をすることはできません。

![スクリーンショット 2025-05-15 22.34.01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3003807/15a88eb2-b571-48c4-8f2d-71805f45410b.png)

`renderer.jsx` を変更した際には、`npm run build:frontend` を再度実行しないとアプリに変更が反映されないことに注意してください。

## Todos モデルの作成
次に、Todo の作成、追加、更新処理を行うためのクラスを作成します。Todo の内容は React のコンポーネントだけでなく MCP サーバからも扱える必要があるので、`renderer.jsx` とは別の `todosModel.js` に作成します。

```sh
touch todosModel.js
```

```js:todosModel.js
class TodosModel {
  constructor() {
    this.todos = [] // { id, title, completed } の配列
    this.nextId = 1 // 次に追加する Todo の id
  }

  // 全ての Todo の配列を返す
  list() {
    return this.todos
  }

  // 名前が title の Todo を新たに追加して、追加した Todo を返す
  add(title) {
    const newTodo = { title, id: this.nextId, completed: false }
    this.todos.push(newTodo)
    this.nextId += 1

    return newTodo
  }

  // 指定された id の Todo を完了済みにする
  complete(id) {
    const todo = this.todos.find((todo) => todo.id === id)
    if (todo) { todo.completed = true }
  }
}

module.exports = { TodosModel }
```

上記の `TodosModel` は次のように使うことができます。

```
$ node
> const { TodosModel } = require('./todosModel')
> const todosModel = new TodosModel()
> todosModel.add('タスク')
{ title: 'タスク', id: 1, completed: false }
> todosModel.complete(1)
> todosModel.list()
[ { title: 'タスク', id: 1, completed: true } ]
```

## Todos モデルと React アプリの接続
これまでに作った内容を繋ぎ合わせて、Todos モデルで管理している Todo を React アプリで表示・変更できるようにします。ただし、MCP サーバでも Todo を利用したいので、`TodosModel` のインスタンスは `main.js` で定義するようにして、`renderer.jsx` は `main.js` と通信することで Todo 一覧を扱えるようにします。

* main → renderer
  * タスクが変更されたときに新しいタスク一覧を renderer に送信する
* renderer → main
  * 現在のタスク一覧の取得やタスクの追加、完了の要求を main に送信する

これを実現するために、Electron が提供する [ipcMain](https://www.electronjs.org/ja/docs/latest/api/ipc-main) と [ipcRenderer](https://www.electronjs.org/ja/docs/latest/api/ipc-renderer) を利用します。詳しくは[プロセス間通信に関する Electron のドキュメント](https://www.electronjs.org/ja/docs/latest/tutorial/ipc)を参考にしてください。

先に `renderer.jsx` 側で利用する機能を `preload.js` に作成します。


```sh
touch preload.js
```

```js:preload.js
const { contextBridge, ipcRenderer } = require('electron')

contextBridge.exposeInMainWorld('todosClient', {
  list: () => ipcRenderer.invoke('list'),
  add: (title) => ipcRenderer.invoke('add', title),
  complete: (id) => ipcRenderer.invoke('complete', id),

  subscribeOnUpdate: (callback) => {
    const wrappedFn = (_, value) => callback(value)
    ipcRenderer.on('update', wrappedFn)

    const unsubscribe = () => { ipcRenderer.off('update', wrappedFn) }
    return unsubscribe
  },
})
```

:::note info
`ipcRenderer` を使う処理を `renderer.jsx` に直接書かないのはセキュリティ上の理由によります。概要が[Electron チュートリアル（プリロードスクリプトの利用）](https://www.electronjs.org/ja/docs/latest/tutorial/tutorial-preload) に記載されています。
:::

次に、`main.js` 内で `ipcMain` を使う処理を書きます。また、`preload.js` の内容が `renderer.jsx` で利用できるように設定します。

```diff:main.js
-const { app, BrowserWindow } = require('electron')
+const { app, BrowserWindow, ipcMain } = require('electron')
+const  path = require('node:path')
+const { TodosModel } = require('./todosModel')
+
+const todosModel = new TodosModel() // ①

 app.whenReady().then(() => {
   const win = new BrowserWindow({
     width: 600,
     height: 600,
+    webPreferences: { // ②
+      preload: path.join(__dirname, 'preload.js'),
+    },
   })

+  // ③
+  const todosApi = {
+    list: () => todosModel.list(),
+    add: (title) => {
+      const newTodo = todosModel.add(title)
+      win.webContents.send('update', todosModel.list())
+      return newTodo
+    },
+    complete: (id) => {
+      todosModel.complete(id)
+      win.webContents.send('update', todosModel.list())
+    }
+  }
+  
+  // ④
+  ipcMain.handle('list', () => todosApi.list())
+  ipcMain.handle('add', (_event, title) => todosApi.add(title))
+  ipcMain.handle('complete', (_event, id) => todosApi.complete(id))
+
   win.loadFile('index.html')
 })
```

- ① `TodoModel` のインスタンスを作ります
- ② `preload.js` の読み込み設定です。この記述をすることで `preload.js` の内容がレンダラー側の JavaScript 実行時に含まれるようになるため、例えば `renderer.jsx` 内で `todosClient.list()` が実行できます
- ③ todos モデルの操作を提供します。Todo 一覧の取得、追加、完了機能を提供しており、Todo 一覧が変更されたときに 'update' メッセージを送信するようになっています。参考: [Electron チュートリアル（メインからレンダーへ）](https://www.electronjs.org/ja/docs/latest/tutorial/ipc#%E3%83%91%E3%82%BF%E3%83%BC%E3%83%B3-3-%E3%83%A1%E3%82%A4%E3%83%B3%E3%81%8B%E3%82%89%E3%83%AC%E3%83%B3%E3%83%80%E3%83%A9%E3%83%BC%E3%81%B8)
- ④ プロセス間通信のメインプロセス側の実装です。`preload.js` で定義したそれぞれの `ipcRenderer.invoke` の呼び出しに対応しています

以上の変更によって、`renderer.jsx` 側で `todosClient` が利用できるようになっています。`todosClient` を実際に使って動的な Todo アプリに修正しましょう。

```diff:renderer.jsx
 import React from 'react'
 import { createRoot } from 'react-dom/client'
 
-const App = () => {
+const App = ({ initialTodos }) => {
-  const todos = [
-    { id: 1, title: 'タスク１', completed: true  },
-    { id: 2, title: 'タスク２', completed: false },
-    { id: 3, title: 'タスク３', completed: false },
-  ]
+  const [todos, setTodos] = React.useState(initialTodos)
 
+  React.useEffect(() => {
+    const callback = (todos) => setTodos(todos)
+    const unsubscribe = todosClient.subscribeOnUpdate(callback)
+    return unsubscribe
+  }, [setTodos, todosClient])
+
-  const addTodo = () => { alert('addTodo: まだ実装されていません！') }
+  const addTodo = (formData) => {
+    const title = formData.get('title')
+    todosClient.add(title)
+  }
-  const completeTodo = () => { alert('completeTodo: まだ実装されていません！') }
+  const completeTodo = (id) => { todosClient.complete(id) }
 

(...中略...)

 
 const root = createRoot(document.getElementById('root'))
-root.render(<App />)
+todosClient.list().then((todos) => root.render(<App initialTodos={todos} />))
```

まず、`App` コンポーネントの引数を変更して最初の todo 一覧 (`initialTodos`）を受け取るようにします。`renderer.jsx` の一番下を見るとわかりますが、ここには `todosClient.list()` の結果が入るようになっています。今の `main.js` で作った `todosModel` では Todo 一覧にひとつも Todo を持たないため、`[]` が渡されることになります。

`React.useEffect` を使っている部分は複雑に感じる人もいるかもしれません。まず、`const callback = ...` では、`main.js` の Todo 一覧が変更されるたびに実行される関数を定義して、次の行で `todosClient.subscribeOnUpdate` を使って登録しています。`subscribeOnUpdate` の戻り値は関数（`unsubscribe`）で、これを実行すると `callback` の登録を解除します。`unsubscribe` を return することによって、`App` コンポーネントの再読み込みや破棄時に `unsubscribe` が実行されるようになります。このような実装によって、`App` コンポーネントが表示されている間は Todo 一覧の変更に対応できるようになっています。

例えば、新しい Todo の名前を入力して「追加」ボタンを押したときには、次の流れで変更が反映されます：

1. `<form>` に書かれた `action` の記述によって、`addTodo` が実行される
1. `addTodo` では `todosClient.add()` を呼び出す
1. `preload.js` の定義によって `ipcRenderer.invoke('add', title)` が実行される
1. `main.js` に書いた `ipcMain.handle('add', ...)` が実行される。この関数が実行時に 'update' メッセージをレンダラーに送信する
1. `renderer.jsx` の `useEffect` 内で定義した `callback` が実行されて、`setTodos` に新しい `todos` がセットされる。これによって `App` コンポーネントの表示が更新される

忘れずに `npm run build:frontend` を実行して、`npm start` で画面を立ち上げましょう。Todo の追加と完了ができるようになっていれば成功です。このアプリでは Todo の完了取消機能は持たないので、一度つけたチェックを外せないことは想定内の挙動です。

## スタイルを設定する

次のようにスタイルを設定します。

```sh
touch style.css
```

```css:style.css
html {
  font-size: 16px;
}

body {
  max-width: 400px;
  margin: 0 auto;
  padding: 24px 0;
  background: #eee;
}

h1 {
  font-size: 1.4rem;
  font-weight: normal;
}

form {
  width: 100%;
  position: relative;
  border-radius: 16px;
  box-shadow: 1px 2px 2px #888;

  input[type="text"] {
    box-sizing: border-box;
    font-size: inherit;
    width: 100%;
    border: 1px solid #333;
    padding: 12px 12px 60px 12px;
    border-radius: 16px;
    background: #f8f8f8;
  }

  button {
    font-weight: bold;
    font-size: inherit;
    color: white;
    background: #3273f6;
    border: none;

    position: absolute;
    right: 10px;
    bottom: 10px;
    padding: 4px 16px;
    border-radius: 8px;
    cursor: pointer;
  }
}

ul {
  display: flex;
  flex-direction: column;
  gap: 8px;
  list-style: none;
  padding: 0;
}

li {
  border-radius: 12px;
}

label {
  display: flex;
  gap: 6px;
  box-sizing: border-box;
  width: 100%;
  box-shadow: 1px 2px 2px #888;
  border: 1px solid #888;
  border-radius: 12px;
  padding: 8px;
  cursor: pointer;
  background: #f8f8f8;

  &:hover {
    text-decoration: line-through;
    filter: brightness(95%);
  }

  &:has(input:checked) {
    text-decoration: line-through;
    cursor: initial;

    &:hover {
      filter: initial;
    }
  }
}
```

`index.html` で `style.css` を読み込みます。

```diff:index.html
       content="default-src 'self'; script-src 'self'"
     />
     <script defer src="./dist/renderer.js"></script>
+    <link href="./style.css" rel="stylesheet" />
     <title>Godo - LLM もあなたも使える Todo アプリ</title>
   </head>
   <body>
```

ここまでの実装で人間向けの Todo アプリは完成です 🎉

![スクリーンショット 2025-05-16 21.26.38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3003807/aefaa1d6-2d4b-4cef-a3eb-bbfd62d10cf0.png)

## MCP サーバを実装する
最後に、デスクトップアプリの立ち上げと同時に MCP サーバを起動するようにします。これによって LLM から Todo 一覧を確認・操作できるようになります。

MCP サーバは次の方針で実装します。

- MCP TypeScript SDK ライブラリの Streamable HTTP 方式を採用
- [express](https://expressjs.com/ja/) を使用して、ローカルホストの 3000 番に MCP サーバエンドポイントを公開する
- サーバを立ち上げるために、`startMcpServer()` 関数を作って、`main.js` からこれを呼ぶことでサーバを立ち上げられるようにする。`startMcpServer` は引数に `todosApi` を受け取るようにすることで、アプリ共通の Todo 一覧を操作できるようにする

実装は次のようになります。実装にあたって [MCP TypeScript SDK README.md](https://github.com/modelcontextprotocol/typescript-sdk/tree/1fd62657510de51fac2572bb5e704a64020b9caa?tab=readme-ov-file#streamable-http) の記述を参考にしています。

```sh
touch mcpServer.js
```

```js:mcpServer.js
const { McpServer } = require('@modelcontextprotocol/sdk/server/mcp.js')
const { StreamableHTTPServerTransport } = require('@modelcontextprotocol/sdk/server/streamableHttp.js')
const { z } = require('zod')
const express = require('express')

const startMcpServer = (todosApi) => {
  const server = new McpServer({ name: 'godo', version: '1.0.0' })

  server.tool('list-all-todos',
    'Todo 一覧を取得する',
    {},
    () => {
      const todos = todosApi.list()
      return { content: [{ type: 'text', text: JSON.stringify(todos) }] }
    }
  )

  server.tool('add-new-todo',
    'Todo を追加する',
    { title: z.string() },
    ({ title }) => {
      const newTodo = todosApi.add(title)
      return { content: [{ type: 'text', text: JSON.stringify({ newTodo }) }] }
    }
  )

  server.tool('complete-todo',
    'Todo を完了済みにする',
    { id: z.number() },
    ({ id }) => {
      todosApi.complete(id)
      return { content: [{ type: 'text', text: `Completed todo (id: ${id})` }] }
    }
  )

  const app = express()
  app.use(express.json())

  app.post('/mcp', async (req, res) => {
    try {
      const transport = new StreamableHTTPServerTransport({
        sessionIdGenerator: undefined,
      })
      res.on('close', () => {
        transport.close()
        server.close()
      })
      await server.connect(transport)
      await transport.handleRequest(req, res, req.body)
    } catch (error) {
      console.error('Error handling MCP request:', error)
      if (!res.headersSent) {
        res.status(500).json({
          jsonrpc: '2.0',
          error: {
            code: -32603,
            message: 'Internal server error',
          },
          id: null,
        })
      }
    }
  })

  const PORT = 3000
  app.listen(PORT, () => {
    console.log(`MCP HTTP Server listening on port ${PORT}`)
  })
}

module.exports = { startMcpServer }
```

コード中で定義している `server` が MCP サーバです。`server.tool()` を呼ぶことでツールを登録しています。`server.tool()` の第１引数にはツール名、第２引数には説明文、第３引数には入力のスキーマ、第４引数にツールの動作を表す関数を入力しています。スキーマの記述には [Zod](https://zod.dev/) を利用しています。

:::note info
`server.tool` では入力のスキーマ以外にも、出力のスキーマやツールのアノテーションを追加することができます。これらを利用することによって LLM がより適切にツールを使えるようになる可能性があります。
参考:
- https://modelcontextprotocol.io/docs/concepts/tools#tool-annotations
- [GitHub：`tool()` の実装](https://github.com/modelcontextprotocol/typescript-sdk/blob/1fd62657510de51fac2572bb5e704a64020b9caa/src/server/mcp.ts#L751-L797)
:::

コード中で定義している `app` が express アプリの本体です。POST リクエストに対する処理を記述しています。最後に 3000 番ポートで HTTP サーバを立ち上げます。

:::note info
本記事の利用ケース（VSCode からの利用）では、筆者の確認の限りでは POST のハンドルのみで問題なく動作しますが、より多くの MCP クライアントから利用する場合には GET, DELETE 等のリクエストをハンドルするようにするのが適切でしょう。利用しない種類のリクエストメソッドについてはエラー（Method not allowed）を返すようにします。[本記事のリファレンス実装](https://github.com/jajimajp/godo/commit/a95e7d2e58e4460bd5bc548a6c95926f12a62e73)ではこれらのメソッドに対応する関数を定義しています。
:::


`startMcpServer` を使うコードを `main.js` に追加すれば実装は完了です。

```diff:main.js
 const { app, BrowserWindow, ipcMain } = require('electron')
 const  path = require('node:path')
 const { TodosModel } = require('./todosModel')
+const { startMcpServer } = require('./mcpServer')


(...中略...)


   ipcMain.handle('complete', (_event, id) => todosApi.complete(id))

   win.loadFile('index.html')
+  startMcpServer(todosApi)
 })
```

## GitHub Copilot からアプリを使用する

VS Code の GitHub Copilot agent mode を利用して、作成した Todo アプリを LLM から利用してみます。

2025/5/16 現在では、通常の VS Code の MCP 設定からでは Streamable HTTP 方式を利用することができないようです。従って、新機能が取り込まれている（、そして安定版ではないのでバグがあるかもしれない）[Visual Studio Code Insiders](https://code.visualstudio.com/insiders/) を利用します。VS Code Insiders をインストールし、GitHub Copilot を設定して [Copilot Chat](https://docs.github.com/ja/copilot/using-github-copilot/copilot-chat) が利用できるようにしてください。

MCP サーバの設定を登録します。VSCode の settings.json を開いて、次の項目を記述してください。

```json:settings.json
    "mcp": {
        "inputs": [],
        "servers": {
            "godo": {
                "type": "http",
                "url": "http://localhost:3000/mcp",
            }
        }
    }
```

:::note info
settings.json の開き方がわからない場合、[Qiita：VS Codeのsettings.jsonの開き方](https://qiita.com/y-w/items/614843b259c04bb91495) を参考にしてください。または VS Code メニューの Preferences > Settings から、検索窓に「mcp」と入力して最初に出てくる「Mcp」項目の「Edit in settings.json」をクリックすることでも編集できます。
:::

それでは Todo アプリを利用してみましょう。MCP サーバの利用時には `npm start` によるアプリを起動したままにしておいてください。

VS Code の settings.json で先ほど記載した項目の上にある「Start」ボタンをクリックしてください。

![スクリーンショット 2025-05-16 23.29.04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3003807/6c3d6b3f-20b5-452e-9403-6c1b224b79c0.png)

Running が表示されたら無事 MCP サーバに接続できています。

![スクリーンショット 2025-05-16 23.29.31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3003807/ee8fe524-6c2a-432e-ad39-7bee9018a3e1.png)

Copilot Chat を開いて、動作モードを「Agent」にしてください。この状態で Todo に関する話を Copilot Chat に入力したときに、適切に Todo アプリを利用できれば成功です ✅

:::note info
すでに複数の MCP サーバを登録している場合、Copilot Chat がどの MCP サーバを利用するべきか適切に判断できない場合があります。そのような場合にはより具体的に今回の MCP サーバを利用するように指示したり、他の MCP サーバを停止または登録解除してみてください。
:::

## おわりに
本記事では LLM と人間の両方が利用できるデスクトップアプリの作り方を解説しました。本記事で作成したデスクトップアプリには、実装に含めたかったものの実装内容を小さくする観点から省いているものが多くあります。
**キーワード**：永続化、TypeScript、ホットリロード、コンポーネントのスタイリング（TailwindCSS, shadcn/ui）
これらを実装することや、　Todo アプリの機能を充実させることなども LLM / デスクトップアプリ開発の理解につながると思います。

本記事が読者のお役に立てれば幸いです。