はじめに
この記事は社内LT向けにまとめたものです。想定読者は「Claudeはまだあまり触ったことがない」方。細かい前提より、"会話だけでこんなものが作れる"という空気感が伝わることを狙っています。
最近の技術動向(主に生成AI・MCP・デザイン系・フロントエンド系)を効率よくキャッチアップしたくて、Qiitaのトレンド記事をピックアップする仕組みを作りました。構成は「読み取り専用のローカルMCPサーバー(qiita-trend)+ Claude Coworkの定期実行」です。
いきなり結論
面白いのは、このMCPサーバーを1行も自分で書いておらず、Claude(Cowork)との対話だけで完成させたこと。要件の壁打ちからAPI調査、設計判断、実装、起動検証までチャットの中で進みました。
「MCPサーバーを作る」と聞くと身構えるかもしれませんが、実態は Claudeと相談しながら進めるだけ です。プログラミングに自信がなくても、"こういうものが欲しい"を言葉にできれば形になります。この記事では、そうして出来上がった機能がどう動くのかを、図とコードで紹介します(コードも読めなくて大丈夫、「これくらいの分量で済むんだ」という空気だけ感じてください)。
なお、MCPそのものの概念(そもそもMCPとは何か、ホスト/クライアント/サーバーの役割、メリット・リスク)については以前、社内勉強会向けで基礎編を書いています。本記事では概念解説は最小限にとどめるので、前提から知りたい方はそちらもどうぞ。
基礎編では、MCPを「AI用のUSB-Cポート」に例えて、AIと外部システムをつなぐ共通ルールとして解説しています。本記事はその実践編にあたる位置づけで、実際にローカルMCPサーバーを1つ作って動かすところまでを扱います。
今回作った機能の全体像
やりたかったのはシンプルで、「Qiitaから直近の人気記事を、関心キーワードで絞って取ってくる」機能です。これをMCPサーバーの1ツールとして実装しました。
基礎編の言葉で言うと、Claude Desktopがホスト、その中のCoworkがクライアント、自作の qiita-trend がサーバーという関係です。今回はこのサーバーを「自分のPC上のローカルプログラム」として動かしています。
最初に作ったツールは search_trend の1つだけです。あとから全体トレンド把握用の popular_tags を足しました。
| ツール | 役割 | |
|---|---|---|
search_trend |
指定キーワードで直近N日・最低ストック数以上の記事を検索し、重複排除してLGTM数の多い順に上位を返す | 初期実装 |
popular_tags |
Qiita全体で記事数の多い人気タグ一覧を取得する(全体トレンドの把握用) | 後から追加 |
ファイル構成は3つだけ。
qiita-trend/
├── index.js # MCPサーバー本体
├── package.json # 依存(@modelcontextprotocol/sdk, zod)
└── README.md # セットアップ手順
このフォルダは、Claude Desktopから参照できる場所に置いておきます。手元では作業用フォルダにまとめて配置しています。
/Users/<あなたのPCユーザー名>/Documents/works/ClaudeDT/ # ← ここは自分の環境に合わせて変える
├── qiita-trend/ # 今回のMCPサーバー
└── log/ # 次回記事で扱う「良かった記事ログ」
<あなたのPCユーザー名> の部分や配置先は各自の環境に合わせて読み替えてください(後述の設定ファイルやログのパスも同様です)。
なぜ「取得のみ」なのに、あえてMCPにしたのか
最初は「読み取りのみだから安全そう、MCPもありかな」と考えていました。ですがClaudeと整理してみると、安全なのは"読み取りのみ"だからであって、MCPだから安全というわけではないんですよね。素のスクリプトでGETするのもMCP経由も、読み取り専用なら危険度は同じくらい低い。
これは基礎編で書いた「MCPそれ自体にセキュリティ機能があるわけではない」という話とも一致します。だから「安全だからMCP」ではなく「どう使いたいか」で選ぶことにしました。
決め手は Coworkの定期実行と組み合わせて、取得から要約まで会話で任せたかったから。素のスクリプトなら自分でデータを読む必要がありますが、MCPにすればClaudeが取得・選別・要約まで一気に面倒を見てくれます。
Qiitaから「トレンド」を取る3つの選択肢
作る前にClaudeに最新のAPI事情を調べてもらったところ、公式APIには「トレンド」専用エンドポイントがなく、取り方は大きく3つありました。
- 公式Qiita API v2: タグやストック数の条件で検索し、人気記事を近似する。安定していて絞り込みと相性が良い
- 非公式のトレンドAPI: 本当にいまバズってる記事が取れるが、非公式なので将来動かなくなるリスクがある
- RSS: 新着追跡はシンプルだが、LGTM数が取れず人気度の判定に弱い
長く安定して回したかったので 公式API v2 を選択しました。「本物のトレンド」ではなく「直近で人気の記事」の近似になりますが、動向キャッチアップという目的には十分です。
機能の中身: search_trend はどう動くか
このツールの動作を順に追います。ポイントは3つです。
- キーワードごとに1リクエストへ分割する(条件を1クエリに詰め込むとQiita側がエラーを返すことがあるため)
- 検索の絞り込みには、Qiita検索で安定して効く
stocks:>=N(ストック数) を使う - 並べ替えには、レスポンスに含まれる
likes_count(LGTM数) を使う
「絞り込みはストック数、ランキングはLGTM数」と指標を役割で分けているのがこだわりです。
コードで見る
土台: サーバー初期化とAPIクライアント
MCP SDKの McpServer + StdioServerTransport の最小構成です。トークンは環境変数から任意で読み、あればヘッダに付けるだけ。
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
const QIITA_API = "https://qiita.com/api/v2";
const TOKEN = process.env.QIITA_TOKEN;
async function qiitaGet(path, params = {}) {
const url = new URL(`${QIITA_API}${path}`);
for (const [k, v] of Object.entries(params)) url.searchParams.set(k, String(v));
const headers = { Accept: "application/json" };
if (TOKEN) headers.Authorization = `Bearer ${TOKEN}`;
const res = await fetch(url, { headers });
if (!res.ok) throw new Error(`Qiita API ${res.status}: ${await res.text()}`);
return res.json();
}
const server = new McpServer({ name: "qiita-trend", version: "1.0.0" });
本体: search_trend
図で示した「キーワードごとに取得 → 重複排除 → LGTM降順」がそのままコードに対応します。
server.tool(
"search_trend",
"指定キーワードで直近のQiita人気記事を取得する(読み取り専用)",
{
keywords: z.array(z.string()).min(1).describe("検索キーワード(例: 生成AI, MCP)"),
days: z.number().int().min(1).max(30).default(7).describe("遡る日数"),
minStocks: z.number().int().min(0).default(5).describe("最低ストック数"),
limit: z.number().int().min(1).max(20).default(10).describe("返す件数"),
},
async ({ keywords, days, minStocks, limit }) => {
const since = new Date(Date.now() - days * 86400_000)
.toISOString()
.slice(0, 10);
const seen = new Map();
for (const kw of keywords) {
const query = `${kw} created:>=${since} stocks:>=${minStocks}`;
const items = await qiitaGet("/items", { query, per_page: 20 });
for (const item of items) seen.set(item.id, item); // 記事IDで重複排除
}
const top = [...seen.values()]
.sort((a, b) => b.likes_count - a.likes_count)
.slice(0, limit)
.map((i) => ({
title: i.title,
url: i.url,
likes: i.likes_count,
stocks: i.stocks_count,
tags: i.tags.map((t) => t.name),
created_at: i.created_at,
}));
return { content: [{ type: "text", text: JSON.stringify(top, null, 2) }] };
}
);
返り値を整形済みJSONにしているのは、受け手がClaudeだから。人間向けの装飾を頑張るより、構造化された素のデータを返してLLM側に要約・整形させたほうが柔軟でした。
補助: popular_tags(後から追加)
初期実装は search_trend だけでしたが、運用しはじめてから「キーワードに依存せず、いまQiita全体で何が多いか」も見たくなり、あとから足したのがこのツールです。
server.tool(
"popular_tags",
"Qiita全体で記事数の多い人気タグ一覧を取得する(読み取り専用)",
{
limit: z.number().int().min(1).max(50).default(20).describe("返すタグ数"),
},
async ({ limit }) => {
const tags = await qiitaGet("/tags", { sort: "count", per_page: limit });
const result = tags.map((t) => ({
name: t.id,
items_count: t.items_count,
followers_count: t.followers_count,
}));
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
}
);
const transport = new StdioServerTransport();
await server.connect(transport);
Claude Desktopへの登録
claude_desktop_config.json に登録して再起動すれば使えます。トークンは任意で、使う場合も read_qiita スコープのみでOKです。基礎編で触れた通り、設定ファイルにはトークンが入りうるので外部公開しないよう注意します。
{
"mcpServers": {
"qiita-trend": {
"command": "node",
"args": ["/Users/<あなたのPCユーザー名>/Documents/works/ClaudeDT/qiita-trend/index.js"],
"env": {
"QIITA_TOKEN": "read_qiitaスコープのみのトークン(任意)"
}
}
}
}
Claudeとの対話だけで作るとどうなるか
開発は次の流れで、すべてチャット上で進みました。
- 要件の壁打ち: 「Qiitaのトレンドをピックアップしたい」から始め、Claudeが「トレンドの定義は?」「絞り込みは?」と選択肢付きで質問してくる
- API調査: Qiita API v2のクエリ仕様・レート制限・非公式APIの存在をClaudeがWeb検索で確認し、3択に整理
- 設計判断: 「スクリプトかMCPか」の比較をClaudeが率直に提示(「取得のみならスクリプトが軽い」と正直に言ってくる)。Cowork定期実行との組み合わせで最終判断
- 実装: MCP SDKの最新仕様とQiitaの検索記法を確認したうえでコード生成
- 検証: stdioでの簡易ハンドシェイク(initialize → tools/list)まで自動で実施し、ツール登録を確認
印象的だったのは、Claudeができていないことを正直に報告してきた点です。開発に使ったサンドボックスはqiita.comへ通信できない設定だったため、「起動・ツール登録・スキーマは確認済みだが、実際に記事が返るかのライブ確認は未実施。手元で疎通確認してほしい」と明示され、READMEに手順が入りました。
セットアップと動作確認
npm install-
claude_desktop_config.jsonにqiita-trendを登録し、Claude Desktopを再起動 - Coworkで「qiita-trend の search_trend で直近4日の上位5件を見せて」と頼み、記事が返れば疎通OK
未認証だとIPあたり1時間60リクエストの制限があるので、毎日回すならトークン発行が安心です。
まとめ
- 「トレンド」の定義を先に決めると、データソース選定がぶれない
- 機能の肝は「絞り込み=ストック数 / ランキング=LGTM数」と「キーワードごとに分割して重複排除」
- MCPは「安全だから」ではなく「AIに取得+要約まで任せたいか」で選ぶ(基礎編の"MCP自体は安全装置ではない"とも一致)
- 実装をAIに任せるなら、「確認済み/未確認」を区別して報告してくれるかが信頼の分かれ目
次回は、このMCPをCoworkの定期タスクにつなぎ、「良かった記事をログに残して好みを学習させる」仕組みに育てた話を書きます。
▶︎続きはこちら
MCP紹介記事
