GaroonとkintoneのMCPサーバーを使ってガルキンポータルを作成しよう

著者名:アシノ( サイボウズ株式会社 (External link)

目次

はじめに

本記事では、Claude for DesktopからGaroonとkintoneのローカルMCPサーバーを利用し、「Garoon×kintone連携によるkintoneCMS(以下、ガルキンポータル)」の構築を自動化する方法を紹介します。

ガルキンポータルとは

「ガルキンポータル」とは、Garoonとkintoneを組み合わせた造語で、kintoneをCMS(コンテンツ管理システム)として活用し、そのデータをGaroonポータルに自動反映させる連携手法を指します。
この手法により、kintoneアプリをGaroonポータルの管理画面として部門情報やリンク集などのコンテンツを一元管理できます。

具体的には、次のような流れでコンテンツを管理します。

  1. kintoneでコンテンツを管理
    部門情報、リンク集、お知らせなどのコンテンツをkintoneアプリで一元管理
  2. Garoonポータルで表示
    kintone REST APIを通じてデータを取得し、Garoonポータルに動的に表示
  3. 簡単な更新作業
    kintoneのレコードを編集するだけで、Garoonポータルの表示内容が自動更新

上記の流れをプロンプトによる指示だけで実行でき、kintoneアプリの設計からGaroonポータル用HTMLポートレットのソースコード生成まで、一気通貫で自動化します。
このようにkintoneをCMSとして活用することで、技術的な知識がなくてもポータルを更新できる環境を実現できます。

前提条件と注意事項

前提条件と注意事項は、次の記事を参照してください。
kintoneとGaroonのMCPサーバーを使ってみよう

イメージ

本記事で紹介するGaroonポータルの完成イメージは、次のとおりです。

実際にGaroonに登録されている組織情報を元に、部門紹介欄とカテゴリー別リンク一覧を表示します。
画像左上の「部門別Garoonポータル(xxx)」をクリックすると、該当部門の情報が表示されます。

kintoneアプリをCMSとして活用するイメージは、次のとおりです。

操作方法と注意事項を記載した「部門別Garoonポータル」アプリを作成し、1部門1レコードで登録します。
アプリのレコードを編集するだけで、Garoonポータルの表示内容が自動更新されます。

システム構成

本記事で紹介するシナリオのシステム構成は、次のとおりです。

プロンプトの用意

設計ポイント

期待どおりの成果物を得るためには、AIが迷いやすいポイントを先回りして明示しておくことが重要です。
今回は次の5つに気を付けてプロンプトを設計します。

  1. 方向性を示す。
    例:「洗練されたスタイリッシュでGaroon UIと調和するデザイン」
  2. 出力形式を指定する。
    例:Garoonポートレットの基本構造、HTML/CSS/JavaScriptのスコープ
  3. 具体例を示す。
    例:表示順序の具体的なテーブル、APIの実装例
  4. 技術的制約を明確化する。
    例:実装テンプレートの提供とコーディング規約/セキュリティルールの明示
  5. タスクを分割する。
    例:6つのセクションに分けて段階的に構造化

実行権限の確認

Claudeの[設定]>[拡張機能]>「拡張機能名(例:Garoon MCP Server)」>[設定]画面の「ツールの権限」項目で、実行できる操作を確認しておきます。
ここにない操作は、AIに依頼しても実行されません。

今回のシナリオに必要なGaroonの操作は、次のとおりです。

  • Get Organizations:Garoon上の組織を取得

また、今回のシナリオに必要なkintoneの操作は、次のとおりです。

  • Add App:動作テスト環境にアプリを作成
  • Add Form Fields:アプリにフィールドを追加
  • Get Form Fields:アプリのフィールドを取得
  • Get Form Layout:アプリのフォームレイアウトを取得
  • Update Form Layout:アプリのフォームレイアウトを更新
  • Update General Settings:アプリの一般設定を変更
  • Deploy App Settings:アプリ設定を運用環境へ反映
  • Get App Deploy Status:アプリ設定の運用環境への反映状況確認
  • Add Records:複数のサンプルデータを追加

サンプルプロンプト

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
/*
 * create garoon portal using mcp servers sample prompt
 * Copyright (c) 2025 Cybozu
 *
 * Licensed under the MIT License
 * https://opensource.org/license/mit/
 */

GaroonとkintoneのMCPサーバーで次の機能を実装する。

## 1. 機能要件
### 1.1 基本機能

- kintoneアプリのデータをGaroonポータルに自動反映
- 上部:部門紹介欄、下部:カテゴリー別リンク一覧
- リンクは新タブで開く

### 1.2 部門紹介欄

- 複数行テキストと画像(右側)を表示

### 1.3 カテゴリー/リンク機能

- カテゴリー設定:カテゴリー名、背景色、表示順序
- カテゴリー内に複数リンクをボタン表示
- 表示順序はリンクの順番にも影響
  - 例1:同カテゴリー名の時、表示順序が高いほど上位に設定
  - 例2:次の場合、「カテゴリー名:業務」が最も最初に設置され、リンクは運用 -> 開発の順に設置

    | カテゴリー名 | リンクタイトル | 表示順序 |
    | ------ | ------- | ---- |
    | 業務     | 開発      | 3    |
    | 業務     | 運用      | 1    |
    | 資料     | ドキュメント  | 2    |

### 1.4 デザイン要件

- 洗練されたスタイリッシュでGaroon UIと調和するデザイン
- カテゴリ別リンク一覧は横に3列とし、それ以上カテゴリが増える場合は2段目に配置
- 極力スクロールが発生しない画面構成
- レスポンシブ対応
- 視認性と操作性を優先

## 2. kintoneアプリ設定

- アプリ名:部門別Garoonポータル
- 説明欄:操作方法と注意事項を記載
- 構造:1部門1レコード、リンク情報はサブテーブルで管理

## 3. サンプルデータ

- Garoonの「部」付き組織から2件取得しテストデータを作成
- HTML内のレコード番号を変更して特定部門を表示

## 4. 成果物

Garoonポートレット用HTMLソースコード

## 5. 技術仕様
### 5.1 Garoonポートレット基本構造

次の構造で作成する。

```html
<div class="{portlet-name}"><!-- ここにHTMLのソースコードを記述する。 --></div>
<style>/* ここにCSSのスタイルを記述する。Garoon本体に影響しないよう、すべてを .{application-id} スコープに限定すること。 */</style>
<script>(() => {/* ここにHTMLソースコードを記述する。 */})()</script>
```

### 5.2 kintone REST API(セッション認証・読み取り専用)

セッション認証でkintoneデータにアクセスする。

```javascript
const resp = await fetch("/k/v1/records.json?app={app-id}&query={query}", { method: "GET", headers: { "X-Requested-With": "XMLHttpRequest" } });
```

詳細は[APIドキュメント](https://cybozu.dev/ja/id/de8fa2714f80a6bdacad6cb4/)参照。

### 5.3 実装上の注意事項

- JavaScript/CSSはスコープを限定し、Class/IDにプレフィックスを付けGaroonへの影響を防ぐ
- kintone GETリクエストはContent-Type未設定
- 原則としてAPIトークンやCORS設定は不要
- 添付ファイル取得:`{ type: "FILE"; value: { contentType: string; fileKey: string; name: string; size: string; }[]; }`
- 画像表示:fetchでBlob取得後、`URL.createObjectURL()`で変換
- リクエストの重複送信を避け負荷を減らす

## 6. コーディングルール

Garoonとkintoneのコーディング時は、次の2つのルール厳守する。

- [kintoneコーディングガイドライン](https://cybozu.dev/ja/id/6a3e8e4cfa544930dea12c84/)
- [kintoneセキュアコーディングガイドライン](https://cybozu.dev/ja/id/da9ec3d69f4d50df6fb94f58/)

追加ルール:

- ES6以降の構文を使用
- 変数宣言はconstまたはletのみ使用(varは禁止)
- 関数はアロー関数で記述
- 非同期処理はasync/awaitを使用(コールバックや`.then`は禁止)
- コメントは必要最低限
- try/catchでエラーハンドリングを行う。その際、catchの引数はerrorとする

コード生成後セキュリティーレビューを行い、問題があれば修正する。

実行方法

  1. Claude画面左上の[サイドバーの切り替え]をクリックし、[新規チャット]をクリックします。
  2. 上記のサンプルプロンプトを入力します。
  3. 実行時に「許可」を求められたら、次のどちらかを選択します。
    • 「常に許可」
      以後の実行が自動承認されるため効率的ですが、誤操作や悪意あるコマンドも実行されるリスクがあります。
    • 「一度だけ許可」
      安全性は高いですが、その都度確認が必要です。
      基本的には「一度だけ許可」がおすすめです。

実行結果の確認

確認ポイントは次の3点です。

  1. HTMLポートレットのソースコードが生成されているか。
  2. kintoneアプリ「部門別Garoonポータル」が作成されているか。
  3. アプリ内にサンプルデータが登録されているか。

ひとつずつ確認していきましょう。

1. HTMLポートレットのソースコードが生成されているか

次のようなHTMLのソースコードが生成されていれば成功です。

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
<!--
* create garoon portal using mcp servers sample program
* Copyright (c) 2025 Cybozu
*
* Licensed under the MIT License
* https://opensource.org/license/mit/
-->

<div class="dept-portal">
  <div id="dept-portal-container"></div>
</div>

<style>
  .dept-portal {
    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial,
      sans-serif;
    color: #333;
    line-height: 1.6;
  }

  .dept-portal * {
    box-sizing: border-box;
  }

  .dept-portal .portal-loading {
    text-align: center;
    padding: 40px;
    color: #888;
    font-size: 14px;
  }

  .dept-portal .portal-error {
    background-color: #fee;
    border: 1px solid #fcc;
    border-radius: 4px;
    padding: 16px;
    margin: 16px 0;
    color: #c33;
    font-size: 14px;
  }

  .dept-portal .dept-intro {
    display: flex;
    gap: 24px;
    margin-bottom: 32px;
    padding: 24px;
    background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
    border-radius: 12px;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
    align-items: center;
  }

  .dept-portal .dept-intro-text {
    flex: 1;
    min-width: 0;
  }

  .dept-portal .dept-name {
    font-size: 24px;
    font-weight: 700;
    margin: 0 0 12px 0;
    color: #2c3e50;
  }

  .dept-portal .dept-description {
    font-size: 15px;
    line-height: 1.7;
    color: #555;
    white-space: pre-wrap;
    word-wrap: break-word;
  }

  .dept-portal .dept-image-wrapper {
    flex-shrink: 0;
    width: 200px;
    height: 150px;
    border-radius: 8px;
    overflow: hidden;
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
    background-color: #fff;
  }

  .dept-portal .dept-image {
    width: 100%;
    height: 100%;
    object-fit: cover;
  }

  .dept-portal .categories-container {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: 20px;
  }

  .dept-portal .category-card {
    background-color: #fff;
    border-radius: 10px;
    box-shadow: 0 3px 10px rgba(0, 0, 0, 0.12);
    overflow: hidden;
    transition: transform 0.2s, box-shadow 0.2s;
  }

  .dept-portal .category-card:hover {
    transform: translateY(-2px);
    box-shadow: 0 5px 15px rgba(0, 0, 0, 0.18);
  }

  .dept-portal .category-header {
    padding: 16px 20px;
    color: #fff;
    font-size: 18px;
    font-weight: 600;
    text-align: center;
    text-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
  }

  .dept-portal .category-links {
    padding: 16px;
    display: flex;
    flex-direction: column;
    gap: 10px;
  }

  .dept-portal .link-button {
    display: block;
    padding: 12px 16px;
    background-color: #f8f9fa;
    border: 1px solid #e0e0e0;
    border-radius: 6px;
    text-decoration: none;
    color: #333;
    font-size: 14px;
    font-weight: 500;
    text-align: center;
    transition: all 0.2s;
    cursor: pointer;
  }

  .dept-portal .link-button:hover {
    background-color: #e9ecef;
    border-color: #adb5bd;
    transform: translateX(4px);
  }

  @media (max-width: 1200px) {
    .dept-portal .categories-container {
      grid-template-columns: repeat(2, 1fr);
    }
  }

  @media (max-width: 768px) {
    .dept-portal .dept-intro {
      flex-direction: column;
    }

    .dept-portal .dept-image-wrapper {
      width: 100%;
      height: 200px;
    }

    .dept-portal .categories-container {
      grid-template-columns: 1fr;
    }
  }
</style>

<script>
  (() => {
    const APP_ID = '555';
    const RECORD_ID = '1'; // 表示したい部門のレコードIDに変更する

    const container = document.getElementById('dept-portal-container');

    const showLoading = () => {
      container.innerHTML = '<div class="portal-loading">読み込み中...</div>';
    };

    const showError = (message) => {
      container.innerHTML = `<div class="portal-error">${escapeHtml(message)}</div>`;
    };

    const escapeHtml = (text) => {
      const div = document.createElement('div');
      div.textContent = text;
      return div.innerHTML;
    };

    const sanitizeColor = (color) => {
      const colorPattern = /^(#[0-9A-Fa-f]{3,8}|rgb\([\d\s,]+\)|rgba\([\d\s,.]+\)|[a-z]+)$/;
      return colorPattern.test(color) ? color : '#cccccc';
    };

    const fetchKintoneRecord = async () => {
      try {
        const response = await fetch(`/k/v1/record.json?app=${APP_ID}&id=${RECORD_ID}`, {
          method: 'GET',
          headers: {
            'X-Requested-With': 'XMLHttpRequest',
          },
        });

        if (!response.ok) {
          throw new Error(`HTTPエラー: ${response.status}`);
        }

        const data = await response.json();
        return data.record;
      } catch (error) {
        throw new Error(`データ取得エラー: ${error.message}`);
      }
    };

    const fetchImage = async (fileKey) => {
      try {
        const response = await fetch(`/k/v1/file.json?fileKey=${fileKey}`, {
          method: 'GET',
          headers: {
            'X-Requested-With': 'XMLHttpRequest',
          },
        });

        if (!response.ok) {
          throw new Error('画像の取得に失敗しました');
        }

        const blob = await response.blob();
        return URL.createObjectURL(blob);
      } catch (error) {
        console.error('画像取得エラー:', error);
        return null;
      }
    };

    const groupLinksByCategory = (linksTable) => {
      const categories = {};

      linksTable.forEach((row) => {
        const categoryName = row.value.category_name.value;
        const categoryColor = row.value.category_color.value;
        const categoryOrder = parseInt(row.value.category_order.value, 10);
        const linkTitle = row.value.link_title.value;
        const linkUrl = row.value.link_url.value;

        if (!categories[categoryName]) {
          categories[categoryName] = {
            name: categoryName,
            color: categoryColor,
            order: categoryOrder,
            links: [],
          };
        }

        categories[categoryName].links.push({
          title: linkTitle,
          url: linkUrl,
        });
      });

      return Object.values(categories).sort((a, b) => a.order - b.order);
    };

    const renderPortal = async (record) => {
      const departmentName = record.department_name.value;
      const departmentDescription = record.department_description.value;
      const departmentImages = record.department_image.value;
      const linksTable = record.links_table.value;

      let imageHtml = '';
      if (departmentImages && departmentImages.length > 0) {
        const imageUrl = await fetchImage(departmentImages[0].fileKey);
        if (imageUrl) {
          imageHtml = `
          <div class="dept-image-wrapper">
            <img src="${imageUrl}" alt="${escapeHtml(departmentName)}" class="dept-image">
          </div>
        `;
        }
      }

      const categories = groupLinksByCategory(linksTable);

      const categoriesHtml = categories
        .map((category) => {
          const linksHtml = category.links
            .map((link) => {
              return `
          <a href="${escapeHtml(
            link.url,
          )}" target="_blank" rel="noopener noreferrer" class="link-button">
            ${escapeHtml(link.title)}
          </a>
        `;
            })
            .join('');

          return `
        <div class="category-card">
          <div class="category-header" style="background-color: ${sanitizeColor(category.color)}">
            ${escapeHtml(category.name)}
          </div>
          <div class="category-links">
            ${linksHtml}
          </div>
        </div>
      `;
        })
        .join('');

      container.innerHTML = `
      <div class="dept-intro">
        <div class="dept-intro-text">
          <h2 class="dept-name">${escapeHtml(departmentName)}</h2>
          <div class="dept-description">${escapeHtml(departmentDescription)}</div>
        </div>
        ${imageHtml}
      </div>
      <div class="categories-container">
        ${categoriesHtml}
      </div>
    `;
    };

    const init = async () => {
      showLoading();

      try {
        const record = await fetchKintoneRecord();
        await renderPortal(record);
      } catch (error) {
        console.error('エラー:', error);
        showError(error.message);
      }
    };

    init();
  })();
</script>

生成されたソースコードは、一度必ず目を通してください。
意図しないコードが含まれていないか、セキュリティ上の問題がないか等を確認し、問題があれば修正してください。

2. kintoneアプリ「部門別Garoonポータル」が作成されているか

kintoneアプリ「部門別Garoonポータル」が作成されているか確認します。
イメージの画像のように、アプリ説明欄に操作方法と注意事項が記載されていれば成功です。

3. アプリ内にサンプルデータが登録されているか

アプリ内にサンプルデータが登録されているか確認します。
イメージの画像のように、2件のレコードが実際のGaroon上に存在する組織名で登録されていれば成功です。

動作確認

HTMLポートレットの表示を確認する

生成されたソースコードをGaroonのHTMLポートレットに追加します。
追加方法は、次のヘルプを参照してください。
HTMLポートレットを追加する (External link)

追加したら、HTMLポートレットを公開する前に、ユーザー画面でどのように表示されるか確認しましょう。
表示の確認方法は、次のヘルプを参照してください。
HTMLポートレットの表示を確認する (External link)

本記事で紹介しているソースコードを適用すると、次のように「営業部」の情報だけ表示されます。

「部門別Garoonポータル」アプリのサンプルデータには、「営業部」と「監査部」が登録されても、ポートレットには「営業部」の情報だけ表示される場合があります。
この場合、HTMLソースコード内のRECORD_IDの値が「1」に設定されるか確認してみましょう。

170
const RECORD_ID = '1'; // 表示したい部門のレコードIDに変更する

RECORD_IDは「部門別Garoonポータル」アプリのレコード番号に対応しています。
「1」に設定されている場合は、RECORD_IDの値を「2」に変更して、再度HTMLポートレットに追加すると、次のように「監査部」の情報が表示されるはずです。

「部門別Garoonポータル」アプリのレコードを編集してHTMLポートレットの動作を確認する

「部門別Garoonポータル」アプリのレコードを編集して、HTMLポートレットの動作を確認してみましょう。
たとえば、「営業部」のレコードに「部門画像」を追加して保存します。
GaroonのHTMLポートレットをリロードして表示を確認すると、イメージの画像のように、画像が表示されました。

他にも、背景色の変更や表示順序の変更など、アプリのレコードを編集して、HTMLポートレットの動作を確認してみてください。

ポータルに配置する

HTMLポートレットの表示を確認できたら、実際にGaroonポータルに配置して公開します。
詳細は、次のヘルプを参照してください。
ポータル作成の流れ (External link)

イメージの画像のように、部門別にポータルを表示したい場合は、部門毎にポータルを追加してください。

おわりに

本記事では、GaroonとkintoneのMCPサーバーを使って、kintoneをCMSとして活用するガルキンポータルを作成する方法を紹介しました。

プロンプトの工夫により、kintoneアプリの設計からHTMLポートレットのソースコード生成まで、一度の実行で完結できることが確認できました。
kintoneをCMSとして活用することで、技術的な知識がなくてもレコード編集だけでGaroonポータルを更新できる環境を実現できます。
生成されたソースコードは必ず内容を確認し、本番環境で利用する場合は権限設定を適切に行ってください。

今回は部門別ポータルの基本的なパターンを紹介しましたが、ぜひプロンプトを工夫してデザインや機能をカスタマイズするなど、いろいろと試してみてください。

関連記事

kintoneとGaroonのMCPサーバーを使ってみよう

information

本記事は、次の環境で動作を確認しています。

  • kintone:2025年11月版
  • Garoon:2025年11月版
  • kintoneローカルMCPサーバー:v1.2.1
  • GaroonローカルMCPサーバー:v1.0.1
  • Claude for Desktop: v1.0.734