この記事は、CYBOZU SUMMER BLOG FES '26の記事です。
kintone Documentation MCPサーバーの開発秘話をご案内します。
Kintone Documentation MCPとは
kintone Documentation MCPサーバーは、AIが最新のkintone公式ドキュメントやAPI情報を常に取得できるツールです。
なんなのか
ChatGPTやClaudeのようなAIプラットフォームを使っていると、モデルはある時点までのデータで学習されていて、それ以降のことは知らない。最近の出来事やソフトウェアのアップデートなど、AIを使っている人なら情報が古いともどかしく感じたことが一度はあるはずです。
Kintone Documentation MCPを使うと、LLMはCybozu Developer Network (以降、Devnet)から直接、最新のドキュメントを検索して取得できるようになる。要は、新しく追加されたAPIや、新しいSDKの変更(cli-kintoneプラグインツールなど)も、LLMが把握できるということです。
ツール
LLMが最初にサーバーに接続する時、MCPプロトコルで定められた初期化(init)ハンドシェイクが行われる。このハンドシェイクで、MCPサーバーは利用可能なツールの情報、パラメータ、使い方をLLMに返す。
MCPサーバーでのToolsを定義する
const TOOLS = [
{
name: "search_docs",
description: "Search the kintone API Documentation. Use when user query involves the Kintone REST API or JS API. Return value does not contain documentation, only meta data, specifically url_path. In order to complete the task, this tool MUST ALWAYS be followed by using the get_page tool with the url_path values of documentation that seems relevant to the user's query.",
inputSchema: {
type: "object",
properties: {
query: { type: "string", description: "The search query" },
lang: { type: "string", enum: ["en", "es", "ja", "zh-tw"], description: "Language of docs to search. Infer from the user's input language." },
limit: { type: "number", description: "Number of results to return (default 10 , max 20)", default: 10, maximum: 20 },
},
required: ["query", "lang"],
},
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true },
},
{
name: "search_resources",
description: "Search the kintone documentation regarding integrations, tutorials, customization, best practices, guides, CLI tools, and actual working examples customizations. Return value does not contain documentation, only meta data, specifically url_path. In order to complete the task, this tool MUST ALWAYS be followed by using the get_page tool with the url_path values of documentation that seems relevant to the user's query.",
inputSchema: {
type: "object",
properties: {
query: { type: "string", description: "The search query" },
lang: { type: "string", enum: ["en", "es", "ja", "zh-tw"], description: "Language of resources to search. Infer from the user's input language." },
limit: { type: "number", description: "Number of results to return (default 10 , max 20)", default: 10, maximum: 20 },
},
required: ["query", "lang"],
},
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true },
},
{
name: "get_page",
description: "Retrieve full content for a kintone documentation page by its URL path. MUST ALWAYS be called after using search_resources or search_docs tools. This tool returns up to date actual documentation in markdown format.",
inputSchema: {
type: "object",
properties: {
path: { type: "string", description: "The URL path of the page to retrieve, found in url_path from search results. e.g. '/ja/kintone/docs/js-api/record/get-record/'" },
lang: { type: "string", enum: ["en", "es", "ja", "zh-tw"], description: "Language of the page. Infer from the user's input language." },
},
required: ["path", "lang"],
},
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true },
},
];
これらのツール名は関数として、LLMからの後続のAPIリクエストで呼び出される。そういう意味で、「MCPサーバー」自体の開発は簡単ですね。
Search Docs / Resources
Search DocsとSearch Resourcesは、S3 Vector BucketにあるDevnetドキュメントに対してベクトル検索を行う。AIはユーザーのクエリを、たとえばこんな形で書くと:
{
"query": "Flutter",
"lang": "ja",
"limit": 5
}
このような値が返ってくる:
[ { "title": "Flutterで作成したiOSアプリにkintoneのレコードをリストで表示しよう!", "description": "Flutterで作成したiOSアプリへkintoneに登録されているレコードをリストで表示するカスタマイズを紹介します。", "url_path": "/ja/kintone/tips/development/3rd-party-services/mobile-app/display-kintone-records-in-flutter-ios-app/", "score": 0.251 },
{ "title": "Vue.js+Vuetify.jsを使って、レコードの一覧と詳細をシングルページで作成しよう!", "description": "Vue.js+Vuetify.jsを使ったkintoneのレコード一覧と詳細画面のカスタマイズ方法を紹介します。", "url_path": "/ja/kintone/tips/development/customize/record-list-customize/spa-view-using-vue-and-vuetify/", "score": 0.141 },
{ "title": "GASとGeminiでGmailのメール内容を要約して顧客リストアプリと連携してみよう!", "description": "Google Apps ScriptとGeminiを使って、Gmailの内容を要約して定期的にkintoneに登録し、顧客リストアプリと連携するカスタマイズを紹介します。", "url_path": "/ja/kintone/tips/development/3rd-party-services/mail/summarize-gmail-contents-with-gemini/", "score": 0.133 },
{ "title": "kintoneに定期的に為替レートを記録して、海外経費を海外為替から自動的に円換算してみよう!", "description": "Google Apps Script(GAS)で為替レートAPIを定期実行してkintoneに為替レートを記録し、海外為替を自動的に円換算する方法を紹介します。", "url_path": "/ja/kintone/tips/development/customize/operations-and-migration/periodic-exchange-rate-recording-using-google-app-script/", "score": 0.13 },
{ "title": "Promiseのかわりにasync/awaitを使ってみよう", "description": "非同期処理をasync/awaitで実行する方法を説明します。", "url_path": "/ja/kintone/tips/development/customize/development-know-how/javascript-customize-middle-async-await/", "score": 0.126 } ]
API Documentationとそれ以外の記事は、S3 Vector Bucket内で別々のDBインデックスに分けています。search_docsはkintone-docsインデックスを、search_resourcesはkintone-resourcesインデックスを検索する。ニュース記事(APIアップデートなど)も、将来のためにkintone-newsインデックスとして別途分離しておきました。
Get Page
get_pageは、実際のドキュメントをユーザーに返すツールです。Devnetの改修で、各ドキュメント記事はHTMLするとともに、別途の/llms.txtエンドポイントも生成されるようにしました。なのでhttps://cybozu.dev/ja/kintone/docs/rest-api/records/get-records/には、https://cybozu.dev/ja/kintone/docs/rest-api/records/get-records/llms.txtが存在する。
{"path":"/ja/kintone/tips/development/3rd-party-services/mobile-app/display-kintone-records-in-flutter-ios-app/","lang":"ja"}
{"text":"[\n {\n \"url_path\": \"/ja/kintone/tips/development/3rd-party-services/mobile-app/display-kintone-records-in-flutter-ios-app/\",\n \"content\": \"\\n### はじめに\\n\\nFlutterはiOSやAndroidアプリの作成が可能なUI Frameworkです。... 以下省略"}
作った背景
Devnetのドキュメントはどうなっているか
Devnetの REST API ドキュメントの典型的な記事を見てみよう。
「複数のレコードを取得する」REST APIのAPI Document
---
title: 複数のレコードを取得する
description: 'クエリで条件を指定して、複数のレコードを取得します。'
tocEndLevel: 4
weight: 400
product: kintone
type: single
layout: single-sidebar-accordion
aliases:
- "/ja/id/ba1703e9391653c667ce958b/"
tryAPI: true
openAPISpec: "getRecords"
---
クエリで条件を指定して、複数のレコードを取得します。
{{ toc }}
<!-- eslint-disable sentence-length -->
<!-- textlint-disable -->
{{< vtable >}}
| HTTPメソッド | GET |
| URL | {{< copy-inline-text >}}https\://sample.cybozu.com/k/v1/records.json{{< /copy-inline-text >}} |
| URL(ゲストスペース)| {{< copy-inline-text >}}https\://sample.cybozu.com/k/guest/`GUEST_SPACE_ID`/v1/records.json{{< /copy-inline-text >}} |
| 認証 | [パスワード認証](/id/edae2a9d04c6e7d0afffda3a/#password), [APIトークン認証](/id/edae2a9d04c6e7d0afffda3a/#api-token), [セッション認証](/id/edae2a9d04c6e7d0afffda3a/#session), [OAuth認証](/id/edae2a9d04c6e7d0afffda3a/#oauth) |
| Content-Type | 不要(リクエストボディにパラメーターを含める場合はapplication/json)|
{{< /vtable >}}
<!-- textlint-enable -->
<!-- eslint-enable sentence-length -->
### リクエストパラメーター {#request-parameters}
| パラメーター名 | 型 | 必須 | 説明 |
| :-- | :-- | :-- | :-- |
| app | 数値または文字列 | 必須 | アプリID |
| fields | 文字列の配列 | 省略可 | レスポンスに含めるフィールドコード<br>省略すると、閲覧権限のあるすべてのフィールドの値が返ります。<br>リクエストボディで`fields`を指定する場合、指定できるフィールドコードの数は1,000件までです。<br>クエリ文字列で`fields`を指定する場合、`fields`に指定できる添字は、0から99までです。 <br>テーブル内のフィールドは指定できません。<br>テーブル内のフィールドを取得する場合は、テーブルフィールドのフィールドコードを指定します。テーブル内のフィールドがすべて取得されます。|
| query | 文字列 | 省略可 | レスポンスに含めるレコードの条件を指定するクエリ文字列<br>クエリ記法の詳細は次のページを参照してください。<br>[クエリの書き方](/kintone/docs/overview/query/)<br>省略すると、閲覧権限のあるすべてのレコードを取得します。 |
| totalCount | 真偽値または文字列 | 省略可 | `query`で指定した条件に一致するレコードの件数を取得するかどうか<ul><li>`true`:件数を取得する</li><li>`false`:件数を取得しない</li></ul>
... 以下省略
記事自体はMarkdownだけど、Hugo用のShortcode、コメントアウト部分、lint用コメント、短縮リンクなど、いろいろと余計な要素が混ざっています。冒頭の「フロントマター」メタデータもあります。このフロントマターがあって、MCPサーバーを割と早く開発進んだのは、長年Devnetチームがコツコツ整理した記事の構成がしっかりしていたためだと思います。
AIユーザーの消費トークン量を減らしたかった
各似たようなシンプルなWeb検索ツールがある。ただ、必要なドキュメントを検索して、そのページをDevnetからMarkdownとしてそのまま取得することで、消費するトークン量を大幅に減らせる。
トークンを減らせると言うのもあるけど、Devnetとしても情報が最新であることも保証できる。このサーバーがなければ、LLMの学習時点か、LLM自身の検索ツールに頼るしかない。
つまり、古い情報を減らして最新のドキュメントを提供することで、ユーザー体験は良くなるし、LLM側もトークン消費が減って、しかも「公式ソース」を提示できるので、時々その非協力的なLLMにとっても信憑性の高い情報源になっていると思います。
DevnetのPVの伸び悩み
もう一つの大きな理由は、自分が「LLM時代におけるユーザー接点の確保」と勝手に今言葉にしてみたことです。実際ここ数年、Devnetのページビューやユーザーとのやり取りは、ユーザのAI利用によって、大きく変動しています。
開発者たちがChatGPTやClaudeのようなサードパーティのプラットフォームに留まったり、あるいはClaude CodeやCursorのようなIDEから一歩も出なかったりすると、Devnetはkintone開発プロセスの中で、API情報やチュートリアル、ニュースのために訪れてもらう重要な場所を失ってしまう。自社サイトをコントロールできることのメリットが当然ですけど、たとえばイベント情報や新製品、あるいは広告や他製品へのアップセルすら訪問者に見せられる(今のところやっていないが)。
MCPサーバーを提供することで、その失っていたユーザーとの接点を、少しだけでも取り戻せるのが大きいと思います。そしてユーザーがkintone開発に関して何を見て、どんな情報を受け取るか影響を与えられる。今のMCPサーバーはシンプルなドキュメント検索ツールにすぎないが、将来的にはMCPとスキルを組み合わせて、有料ソリューションへ誘導したり、サードパーティやパートナーのソリューションと接続したりすることもできるかもしれない(繰り返すが、今はやっていない。ただ、その手段をとっておくことがあるのはいいことですね)。
実装
注意
しばらく技術トークが続きます。飛ばしたい人は下記の #Roadmap あたりから再開してください。
エンジニアリレーション(開発者向けドキュメント)チームには、半分エンジニア、半分ライター/イベント担当、みたいな不思議な人種が揃っています。(動物園で保護されるべき、非常にレアタイプだと思います。)
そういうこともあって、社内ツールに関しては、仕様や拡張性よりも、使い勝手を優先しています。
過去に、社内ツールをすべてAWS SAMアーキテクチャに移行する作業を担当したことがある。エンジニアリレーション(メンバーはだいたいLambdaやAPI Gateway、S3バケットなどを触った経験があるので、とりあえずそれで。
初期のPOCバックエンド
Markdownのパース
最初の目標は、Markdownファイルをパースすることだった。前述の通り、検索には関係ないコメントやlintのメモ、独自コードなどの「ノイズ」がたくさんある。そして、どの記事にも貴重な「フロントマター」が冒頭にある。
記事は一律同じ形式になっているので、AIに生成させた雑なRegexをとりあえず使うのが楽でした。
function parseFrontmatter(text) {
const match = text.match(/^---\s*\n([\s\S]*?)\n---\s*\n/);
if (!match) return { meta: {}, body: text };
const meta = {};
for (const line of match[1].split("\n")) {
const colon = line.indexOf(":");
if (colon === -1) continue;
const key = line.slice(0, colon).trim();
const val = line.slice(colon + 1).trim().replace(/^['"]|['"]$/g, "");
if (key) meta[key] = val;
}
return { meta, body: text.slice(match[0].length) };
}
地味なコードだが、たとえばこんなフロントマターを
---
title: Synchronize User Data with Okta's User Provisioning Feature
description: This article introduces how to use Okta's User Provisioning feature to sync Okta users with Kintone.
tags: ['Okta']
product: kintone
hasToc: true
---
こう変換する:
{
"key": "en/tutorials/authentication-and-user-management/synchronize-user-data-with-oktas-user-provisioning-feature",
"vector": [".0232832, -0.670836", ... 省略],
"metadata": {
"page_id": "en/tutorials/authentication-and-user-management/synchronize-user-data-with-oktas-user-provisioning-feature",
"description": "This article introduces how to use Okta's User Provisioning feature to sync Okta users with Kintone.",
"language": "en",
"url_path": "/en/tutorials/authentication-and-user-management/synchronize-user-data-with-oktas-user-provisioning-feature/",
"product": "kintone",
"title": "Synchronize User Data with Okta's User Provisioning Feature",
"score": 0.251
}
}
ただ、記事のサイズにかなりばらつきがある、という問題があった。JS APIのget-record記事は50行くらいだが、cybozu cdnの記事は1400行くらいある。(実際、開発上の問題のほとんどはこれが原因だった)
チャンキング
ここでチャンキングの話になる。ここ数年AIの利用をなんとなく避けてきたが、積極的に使い始めることにした。なにせ急に「AI担当」になったわけで。設計やAWS Bedrockの制限について相談していたとき、LLMから「AWS Bedrockはベクトル化にあたって長すぎる文字列を拒否するので、チャンキング、つまり記事を分割する処理を実装すべき」と指摘があった。
自分たちの記事はMarkdownで、見出し(#, ##, ###)による自然な流れがあるので、それをもとに分割して、それぞれをベクトル化して保存すればいい、という話になった。
直感的に良さそうに思えたが、この後読んでもらえば分かると、結果的にはかなり時間の無駄をしてしまったことになった。Markdownをパースした後、見出しをもとに記事を分割し、うまく分割できない場合は1万文字ごとに機械的に切る、という方式にした。
巨大なJSONの使用
そして最終的に、devnetにはおよそ8000件(小さい数字)の記事があるが、最初のPOCではデータベースのことなど気にせず、すべての情報を40MBのJSONファイル(大きい数字)に丸ごと保存することにしました。
ドキュメント検索(チャンクのテキスト付き)
また、サーバー側では、検索ツールはユーザーのクエリをAWS Bedrockでベクトル化し、ベクトルを比較して、上位5件の結果とそのdevnet URLパスを返す、という単純な仕組みだった。最初のPOCでは記事本文(あるいはそのチャンクの内容)もまるごと返していたが、これだと300〜400トークンのリクエストが、あっという間に1万〜2万トークンになってしまうことにすぐ気づいた。Oops!
完成したもの
フィードバック
社内テストの後、ソフトβリリースを行い、Kintone MCPサーバーβ版をリリースして、Twitterや販促メールで告知した。反応はそれなりに良かった。
6/15(月)リリース後1週間の反応
X告知の反応も強かった12,813 imp
kintone Documentation MCPサーバー(β) 1,705 PV
自分にとって一番おもしろかったのはここからで、リリース直後から貴重なフィードバックが届き始めた。
ドキュメントはそもそもチャンク化もベクトル化もしなくていい
一番刺さったフィードバックは、リリースしたらすぐ届いたものでしたが、記事のサイズにばらつきがあるせいで、長い記事はAPIドキュメントのような短い記事に比べて検索結果でかなり有利になってしまう、という問題があった。では、どうすればばらつきの記事を対等に扱えばいいでしょうか?
答えは、記事本文をそもそもベクトル化しないことだった。どの記事にもタイトルと説明文があり、長さも似たようなもので、しかも「dify」や「連携コネクター」「レコード取得」のようなキーワードが必ずタイトルに含まれている。だったら記事本文は無視して、タイトルと説明文、それに必要に応じてキーワード(まだ未実装)だけをベクトル化すればいい。そのほうがずっと関連性の高い検索結果ができました。 Less is more、ということでした。
上限、-1件などの話
コミュニティフォーラムでも見落としに関するフィードバックをいくつかもらった(誰かがわざわざlimit: 0で手動のHTTPリクエストを送ってきた。AIなら絶対やらないが、ユーザーは好きにできる)。他にも、検索の関連度スコアを返すようにする、といったQOL的な改善要望があって、とてもMakes Senseでしたのですぐ取り入れました。
Roadmap
検討段階ではありますが、ユーザーにとって役立ちそうだと個人的に思っている機能をいくつか挙げる。順番は公式な優先順位ではなく、あくまで「良さそう」順です:
新しいツール
check_updates
ユーザーが既存のカスタマイズやプラグインのコードを貼り付けると、ツールがkintoneの実際のAPI変更履歴やアップデート履歴を確認して、その後変更された点、非推奨になった点、より良い方法に置き換えられた点をフラグ付けし、関連する修正案を出力する。
check_query
これもフィードバックから。kintoneのクエリ構文は、時々地味にイライラする。特に自分にとっては。LLMは自信満々に間違えて、よく無駄な回り道をさせてくる。何らかの形でデモアプリに対してクエリを検証できるツールがあれば?将来的にエージェントで使えるかもしれない。
check_errors
これはフィードバックから出てきたアイデアだが、ユーザーがエラーメッセージと関連コードを貼り付けたら、AIが原因を特定できたら便利ではないか、という話。ただ、既存のエラーコードに関する知識や、いくつかの実例を事前知識として持っておく必要がありそうなので、MCPサーバーよりスキルのほうが向いているかもしれない。
plan_app
これはMCPサーバーの機能というより、スキルの領域かもしれない。ただ、実際にアプリを作成できるkintone MCPサーバーと組み合わせると、「ドキュメントを検索する」というより「アプリを作る」という話になってきて、これは明らかにもっと大きな話になる。とはいえ、これにはもっと多くの計画とコンテキストが必要で、事前知識をたくさん持ったエージェントフレームワークが必要になると思う(たとえばDevnetの新しいアンチパターンシリーズをチェックしてみてほしい)。
情報元を増やす
現状、公式のdevnetドキュメントしか扱っていない。ただ、
-
コミュニティフォーラムのスレッド — コミュニティは重要で、かなり独特な情報元だと思っていて、これはとても役立つ可能性があると考えている。「問題→解決」型のコンテンツも多く、LLMを使う人の多くは、問題があって解決策が欲しい、という状況にあることが多い
-
ヘルプサイト — ヘルプサイト自体は、設定や権限、組織レベルの理由でカスタマイズがうまくいかないことがある、といった、より一般的なkintoneの知識を補うのに役立つはず
-
サードパーティ・パートナー情報 — これは……かなり議論する必要がある。ただ、エコシステム全体への深い知識を持った、より大きなkintoneエージェントを作るという意味では、なかなか価値がありそうに思える。もちろん、パートナーAになぜパートナーBの製品がレコメンドされたのか説明するのはそれなりにややこしいだろうが……ただ「シニアレベルのkintoneエンジニア」的なエージェントを目指すなら、こうした知識もなんらかな形で含める必要があると思います。
終わりに
Kintone Documentation MCPはまだ入れていない方は公開記事の通りインストールしてみてください。どんどん改善していくし、少人数のチームでフィードバック納得すれば大体すぐ取り入れています。
MCPはこれからのシニアKintoneエージェントを作るのに第一歩だと思っています。
正確・最新の情報を参照できるMCP
幅広い専門的な知識のSkills
Kintone MCP
Local LLMをちょっとPost Trainingして
この4つの要素をうまくできれば、面白いものができるんじゃないかなと。
See you next time.
- Sean

