← ./articles-ja

MCPサーバーでMarkdown知識ベースを配信する設計: HTMLに変換しない理由

Markdownで書いた知識ベースを、WebサイトにもAIコーディングツールにも読ませたいことがあります。

このとき迷いやすいのが、MCPサーバーから何を返すかです。MarkdownをHTMLへ変換して返すべきか、それともMarkdownのまま返すべきか。

結論から言うと、知識ベース用途ではMCPサーバーはraw Markdownを返し、HTML変換はWeb側に任せる設計が扱いやすいです。MCP(Model Context Protocol)は、AIアプリケーションが外部のデータやツールに接続するための標準プロトコルです。表示形式までMCPサーバーに背負わせると、最初は便利でも、クライアントが増えたときに保守が重くなります。

この記事では、実際にMarkdown記事をMCPサーバーから配信できることを確認したうえで、なぜHTMLに変換しない設計がよいのかを整理します。

確認したこと

ローカルのMarkdown知識ベースに対して、stdio transportのMCPサーバーを起動し、MCP SDKのクライアントから次の3つのツールが見えることを確認しました。

list_articles
get_article
search_knowledge

さらに get_article を呼び出すと、記事のmetadata(タイトル、日付、slug、tagsなど)と本文がJSONで返りました。本文はHTMLではなくMarkdownです。検証時点では、取得した本文に <h1><h2><p> のようなHTMLタグは含まれず、Markdownの本文として扱える状態でした。

つまり、「Markdownファイルを1つ置き、それをMCPサーバーからAIクライアントへ配信する」構成は実際に成立します。

推奨設計

おすすめの構成は、次のように責務を分ける形です。

content/*.md
  -> MCP server: raw Markdownを返す
  -> Next.js site: MarkdownをHTMLへ変換して表示する
  -> AI coding tool: Markdownをそのまま読む

MCPサーバーは、Markdownファイルを読み、frontmatter(記事のメタ情報)と本文を返すだけにします。Webサイト側は、remark などのMarkdown処理ライブラリでHTMLへ変換します。AIコーディングツールやエディタ拡張は、Markdownのまま読めます。

この分離のポイントは、MCPサーバーを「表示エンジン」ではなく「知識の配信口」にすることです。

なぜHTMLを返さないのか

HTMLを返すMCPサーバーも作れます。Webサイトだけがクライアントなら、それでも動きます。

ただし、MCPの用途ではHTMLが常に最適とは限りません。Claude CodeのようなAIコーディングツールは、HTMLよりMarkdownのほうが読みやすく、引用もしやすいです。モバイルアプリやデスクトップアプリに展開する場合も、HTMLをそのまま表示するより、ネイティブUIや独自コンポーネントへ変換したい場面があります。

MCPサーバーがHTMLを返すと、サーバーがWeb表示の都合に引きずられます。

  • 見出し構造を変えるたびにMCP側の変換ロジックが影響を受ける
  • Web用CSSに依存したHTMLがAIクライアントへ渡る
  • 別クライアント向けに再変換が必要になる
  • Markdown本文よりトークン量が増えやすい

知識ベースの原本がMarkdownなら、MCPはその原本を返すほうが自然です。

実装の形

最小構成では、MCPサーバーに3つのツールを用意します。

list_articles()
get_article(slug, lang)
search_knowledge(query)

list_articles() は一覧表示や検索候補に使うため、本文を返さずmetadataだけを返します。たとえば slugtitledatedescriptiontagslang です。

get_article(slug, lang) は、指定された記事のmetadataとMarkdown本文を返します。ここでHTMLへ変換しないことが重要です。

search_knowledge(query) は、タイトル、説明文、本文に対して簡単な検索を行い、該当記事のmetadataを返します。全文を毎回返さないことで、AIクライアント側でも必要な記事だけを追加取得できます。

疑似コードにすると、MCP側の考え方は次のようになります。

type Article = {
  slug: string;
  title: string;
  date: string;
  description: string;
  tags: string[];
  lang: "en" | "ja";
  body: string; // raw Markdown
};

function getArticle(slug: string, lang: "en" | "ja"): Article | null {
  const file = findMarkdownFileBySlug(slug, lang);
  if (!file) return null;

  const { data, content } = parseFrontmatter(file);

  return {
    slug: data.slug,
    title: data.title,
    date: data.date,
    description: data.description,
    tags: data.tags ?? [],
    lang: data.lang ?? "en",
    body: content,
  };
}

一方、Webサイト側では同じMarkdown本文をHTMLへ変換します。

import { remark } from "remark";
import html from "remark-html";

export async function renderArticleHtml(markdown: string): Promise<string> {
  const processed = await remark().use(html).process(markdown);
  return processed.toString();
}

このように分けると、MCPサーバーはMarkdownの読み出しと検索に集中できます。表示の都合は、それぞれのクライアントが持てばよいです。

frontmatterは共通契約にする

Markdown知識ベースでは、frontmatterをクライアント間の共通契約にすると安定します。

---
title: "Article title"
date: "2026-06-23"
slug: "article-slug"
description: "Short description for lists and metadata."
tags: ["mcp", "markdown"]
lang: "ja"
---

Article body...

Webサイトは title をページタイトルやH1に使えます。MCPサーバーは slug を記事取得のキーにできます。descriptiontags は検索結果や記事一覧に使えます。

ここで注意したいのは、frontmatterの解釈をWeb側とMCP側で揃えることです。たとえば draft: true の記事をWebサイトでは非公開にしているのに、MCPサーバーでは返してしまうと、公開前の情報がAIクライアントに見えてしまいます。

同じMarkdownを複数の読み手で使うなら、次のルールは揃えておくべきです。

  • 必須項目: titledateslugdescription
  • draft: true はすべての公開面から除外する
  • lang で言語を分ける
  • slug はURLとMCP取得キーの両方で使う
  • tagsは小文字のkebab-caseに揃える

静的サイトとの相性

この設計は、Next.jsの output: export のような静的書き出しとも相性がよいです。

Webサイトはビルド時にMarkdownをHTMLへ変換し、静的HTMLとして配信できます。MCPサーバーはローカル開発環境や社内ツール側でMarkdownを直接読みます。読者向けのWeb配信にはデータベースも実行時APIも不要です。

Next.jsの静的書き出しでは、配信時にNode.jsサーバーが存在しません。そのため、RSS、sitemap、記事HTMLはビルド時に確定させる必要があります。この点は別記事の Next.jsのoutput: exportで詰まる5つの罠と回避策 で詳しく整理しています。

また、開発中に next build は通るのに next dev だけCSSで壊れる場合は、MCPやMarkdown設計ではなく環境変数の問題かもしれません。該当する場合は next devだけCSSが壊れる原因はNODE_ENV=productionだった も確認してください。

セキュリティと公開範囲

Markdown知識ベースをMCPで配るときは、公開範囲を明確にします。

特にローカルMCPサーバーは便利ですが、接続したAIクライアントにMarkdown本文を渡します。記事内に秘密情報、内部URL、個人情報、APIキー、顧客名、未公開の事業情報が入っていれば、そのままコンテキストとして渡る可能性があります。

最低限、次をチェックしてください。

  • content/ に秘密情報を置かない
  • draft: true をMCP側でも除外する
  • CONTENT_DIR のような設定で、読むディレクトリを明示する
  • パス指定を受け取る設計にする場合は、slugから安全に解決する
  • stdoutはMCPのJSON-RPC通信に使うため、ログはstderrへ出す

MCPは外部データをAIに接続するための仕組みです。便利さと同時に、「何を読ませるか」を設計上の責務として扱う必要があります。

まとめ

Markdown知識ベースをMCPサーバーで配信するなら、MCP側でHTMLへ変換しない設計が保守しやすいです。

MCPサーバーはraw Markdownとmetadataを返す。WebサイトはMarkdownをHTMLへ変換する。AIコーディングツールはMarkdownをそのまま読む。この分担にすると、1つのMarkdown原稿を複数の読み手で共有できます。

実機確認でも、MCPクライアントから list_articlesget_articlesearch_knowledge を呼び出し、get_article がHTMLではないMarkdown本文を返すことを確認できました。

MCPサーバーを「何でも変換するAPI」にするのではなく、「原本を安全に渡す薄い配信レイヤー」にする。Markdown知識ベースでは、このくらい単純な設計のほうが長く使えます。

参考