最近、各所でMCPの話題を耳にしますよね。
どんどん色んな応用例などの知見が共有され始めていて、MCPの持つパワーはまだ底知れずな感じがしています。
MCP (またはそれに類似する技術) とはおそらくこの先長い間付き合っていくことになるだろうと読んでいるので、今のうちから技術キャッチアップしておくことが非常に重要だと思っています。
そこで、今回は一旦そのポテンシャルを感じるために、簡易的なMCP Serverの実装をしてみました。
やったことは単純で、GitHub GistとMCP Clientとを繋ぐMCP Serverを作って、自然言語でGistを扱ってみよう、みたいな感じです。
大部分をClineくんとお茶しながら進める方式で実装したので、多少読みづらい部分があるかもしれませんが、そこはご愛嬌ということで。
Ubuntu 20.04 on WSL2 on Win11
MCP Client
MCP Server
GitHub Gist
ファイル構成は以下の通りです。(細かい設定ファイルは省いています)
.
├── dist
├── package.json
├── package-lock.json
├── src
│ ├── gist.ts
│ ├── main.ts
│ └── types.ts
└── tsconfig.json
この記事では、MCP Serverの実装に関連する部分だけをピックアップして話します。
それでは細かい実装を見ていきましょう。
types.tsでは、MCP ServerのRequest、Responseに対応するSchemaを中心に今回扱う型を定義しています。
後述しますが、MCP ServerのRequestはzodを利用して定義できます。 (ありがたい)
Request schemaのdescriptionはLLMがどのような値を入れるといいのかを認識するために利用していそう (多分) なので、丁寧に書くのが良さそうです。
import { z } from 'zod';
export type Gist = {
id?: string;
description?: string | null;
html_url?: string;
files?: GistFile[];
public?: boolean;
created_at?: string;
updated_at?: string;
comments?: number;
owner?: {
login: string;
avatar_url: string;
} | null;
};
export type GistFile = {
filename?: string;
type?: string;
language?: string | null;
raw_url?: string;
size?: number;
content?: string;
};
export const ListGistsRequestSchema = z.object({
// 今回は自身のGitHubのユーザーIDをデフォルト値にいれています
user_id: z.string().default("tea1013").describe('GitHubのユーザーID'),
per_page: z.number().min(1).max(100).default(30).describe('1ページあたりの取得件数'),
page: z.number().min(1).default(1).describe('ページ番号'),
});
export type ListGistsRequest = z.infer<typeof ListGistsRequestSchema>;
export type ListGistsResponse = Gist[];
export const CreateGistRequestSchema = z.object({
content: z.string().min(1).describe('gistに登録するコード'),
description: z.string().optional().describe('gistの説明 (ここを読むだけである程度コードの内容が理解できる程度に記述すること)'),
filename: z.string().min(1).describe('gistに登録するファイル名'),
public: z.boolean().default(false).describe('gistを公開するかどうか'),
});
export type CreateGistRequest = z.infer<typeof CreateGistRequestSchema>;
export type CreateGistResponse = {
url: string | undefined;
html_url: string | undefined;
};
export const GetGistRequestSchema = z.object({
gist_id: z.string().describe('取得するgistのID'),
});
export type GetGistRequest = z.infer<typeof GetGistRequestSchema>;
export type GetGistResponse = Gist;
gist.tsでは、types.tsで利用した型とGitHub APIを利用してGistを操作する処理を書きます。
といっても、LLM自体の操作など難しい実装はなく、本当に素朴にRequestの情報を利用してAPIを叩いて、素直にResponseを返すだけなので、特筆することもないですね。
import { Octokit } from '@octokit/rest';
import {
CreateGistRequest,
GetGistRequest,
GetGistResponse,
CreateGistResponse,
ListGistsRequest,
ListGistsResponse,
} from './types.js';
// GitHub API Clientを取得する
async function getOctokit(): Promise<Octokit> {
const token = process.env.GITHUB_PERSONAL_ACCESS_TOKEN;
if (!token) {
throw new Error('GITHUB_PERSONAL_ACCESS_TOKEN is not set');
}
return new Octokit({ auth: token });
}
// Gist一覧を取得する
export async function listGists(params: ListGistsRequest): Promise<ListGistsResponse> {
const octokit = await getOctokit();
const response = await octokit.gists.list({
username: params.user_id,
per_page: params.per_page,
page: params.page,
});
const gists = response.data.map(gist => ({
id: gist.id,
description: gist.description,
html_url: gist.html_url,
files: Object.values(gist.files).map(file => ({
filename: file?.filename,
type: file?.type,
language: file?.language,
raw_url: file?.raw_url,
size: file?.size,
})),
public: gist.public,
created_at: gist.created_at,
updated_at: gist.updated_at,
comments: gist.comments,
owner: gist.owner
? {
login: gist.owner.login,
avatar_url: gist.owner.avatar_url,
}
: null,
}));
return gists;
}
// Gistをid指定で取得する
export async function getGist(params: GetGistRequest): Promise<GetGistResponse> {
const octokit = await getOctokit();
const response = await octokit.gists.get({
gist_id: params.gist_id,
});
const gist = response.data;
if (!gist.files) {
throw new Error('Gist not found');
}
return {
id: gist.id,
description: gist.description,
html_url: gist.html_url,
files: Object.values(gist.files).map(file => ({
filename: file?.filename,
type: file?.type,
language: file?.language,
content: file?.content,
raw_url: file?.raw_url,
size: file?.size,
})),
public: gist.public,
created_at: gist.created_at,
updated_at: gist.updated_at,
comments: gist.comments,
owner: gist.owner
? {
login: gist.owner.login,
avatar_url: gist.owner.avatar_url,
}
: undefined,
};
}
// 新たにGistを作成する
export async function createGist(params: CreateGistRequest): Promise<CreateGistResponse> {
const octokit = await getOctokit();
const response = await octokit.gists.create({
files: {
[params.filename]: {
content: params.content,
},
},
description: params.description,
public: params.public,
});
return {
url: response.data.url,
html_url: response.data.html_url,
};
}
最後に、これまで実装したものを利用して、MCP Serverを構築します。
SDKを利用してMcpServerを構築したのち、gist.tsで実装したtoolを追加していきます。
このとき、toolのRequestにはtypes.tsで実装したzod schemaを直接指定できます。
各々のツールは、{ content: [...] }
というオブジェクト形式で結果を返却します。
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { createGist, getGist, listGists } from './gist.js';
import { CreateGistRequestSchema, GetGistRequestSchema, ListGistsRequestSchema } from './types.js';
export class GistServer {
private server: McpServer;
constructor() {
this.server = new McpServer({
name: 'gist-server',
version: '0.1.0',
});
this.setupTools();
}
private setupTools() {
this.server.tool(
'list_gists',
'ユーザーIDを指定してgistの一覧を取得します。',
ListGistsRequestSchema.shape,
async params => {
try {
const gists = await listGists(params);
return {
content: [
{
type: 'text',
text: JSON.stringify(gists, null, 2),
},
],
};
} catch (error) {
return {
content: [
{
type: 'text',
text: `Failed to list gists: ${error instanceof Error ? error.message : String(error)}`,
},
],
isError: true,
};
}
}
);
this.server.tool(
'get_gist',
'gist idを指定してgistの内容を取得します。',
GetGistRequestSchema.shape,
async params => {
try {
const gist = await getGist(params);
return {
content: gist.files
? gist.files.map(file => ({
type: 'text',
text: `Filename: ${file.filename}, Language: ${file.language ?? 'None'}, Description: ${gist.description ?? 'No description'}, Content: ${file.content}`,
}))
: [],
};
} catch (error) {
return {
content: [
{
type: 'text',
text: `Failed to get gist: ${error instanceof Error ? error.message : String(error)}`,
},
],
isError: true,
};
}
}
);
this.server.tool(
'create_gist',
'gistにコードを登録します',
CreateGistRequestSchema.shape,
async params => {
try {
const result = await createGist(params);
return {
content: [
{
type: 'text',
text: `Gist created successfully!\nURL: ${result.url}\nHTML URL: ${result.html_url}`,
},
],
};
} catch (error) {
return {
content: [
{
type: 'text',
text: `Failed to create gist: ${error instanceof Error ? error.message : String(error)}`,
},
],
isError: true,
};
}
}
);
}
async run(): Promise<void> {
const transport = new StdioServerTransport();
await this.server.connect(transport);
console.log('Gist MCP server running on stdio');
}
}
const server = new GistServer();
server.run().catch(console.error);
次に、実装したMCP Serverを動作させるためのセットアップを行います。
Cursorを利用している場合は、MCPのサーバー設定に以下のような形式で記述します。 WSLを利用している関係上、少し特殊になっているので、Macやその他の環境の場合は適当に読み替えてください。
環境変数の GITHUB_PERSONAL_ACCESS_TOKEN
については https://github.com/settings/personal-access-tokens で適当に作って貼り付けます (gistのRead、Write権限必須)
また、今回のプロジェクトでは、build結果をdist/main.js
に配置されるように設定しています。
{
"mcpServers": {
"gist": {
"command": "wsl.exe",
"args": [
"GITHUB_PERSONAL_ACCESS_TOKEN=${YOUR_TOKEN}",
"/path/to/node",
"/path/to/your/repository/dist/main.js"
]
},
}
}
実装したら、一度Cursorを再起動します。(多分そうしないとMCP Serverが適切に読み込まれなさそう?)
再起動したのち、MCP Serverの設定が正しく行えていることを確認します。
それでは、実際にMCP Serverを利用してGistとやりとりをしてみましょう!
まずは、適当にGistにコードを追加してみましょう。
ツールを使いたそうにしているので、Run Tool
を押して実行してもらいましょう。
追加できたみたいです。実際にGistを見に行ってみましょう。
いいじゃないですか!!期待通りに動いていそうです!
では次に、この登録したGistから、MCPサーバーの雛形を出力してもらうという、少し応用的なことをお願いしてみましょう。
いい感じに動いていそうです! 実際には以下のような結果を出力してくれました。
概ね問題なさそうに見えます (見えるだけかも)
// main.ts
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { z } from 'zod'; // スキーマ検証用
// スキーマ定義
const ExampleRequestSchema = z.object({
param1: z.string().describe('パラメータ1の説明'),
param2: z.number().optional().describe('オプションのパラメータ2の説明'),
});
// サービス実装関数
async function exampleFunction(params: z.infer<typeof ExampleRequestSchema>) {
// 実際の処理をここに実装
const result = `パラメータ: ${params.param1}, ${params.param2 ?? 'なし'}`;
return result;
}
export class MyMcpServer {
private server: McpServer;
constructor() {
this.server = new McpServer({
name: 'my-mcp-server', // サーバー名
version: '0.1.0', // バージョン
});
this.setupTools();
}
private setupTools() {
// ツール登録例
this.server.tool(
'example_tool', // ツール名
'このツールの説明文', // ツールの説明
ExampleRequestSchema.shape, // パラメータスキーマ
async params => {
try {
// 実際の処理呼び出し
const result = await exampleFunction(params);
// 成功レスポンス
return {
content: [
{
type: 'text',
text: result,
},
// 他の形式のコンテンツも追加可能
// {
// type: 'image',
// url: 'https://example.com/image.png',
// }
],
};
} catch (error) {
// エラーレスポンス
return {
content: [
{
type: 'text',
text: `エラーが発生しました: ${error instanceof Error ? error.message : String(error)}`,
},
],
isError: true, // エラーフラグをセット
};
}
}
);
// 必要に応じて複数のツールを登録
}
async run(): Promise<void> {
// 標準入出力をトランスポートとして使用
const transport = new StdioServerTransport();
// サーバー接続
await this.server.connect(transport);
console.log('MCP server running on stdio');
// 他のトランスポートの例:
// const httpTransport = new HttpServerTransport({ port: 3000 });
// await this.server.connect(httpTransport);
}
}
// サーバーインスタンス作成と実行
const server = new MyMcpServer();
server.run().catch(console.error);
他にも色々とおもしろいことが出来そうな気がしますが、今回のところは一旦この辺までにしておきます。
MCPのパワーは底知れずだということが今回の例でよく伝わったのではないかと思います。
実装自体はとても簡単なので、一度適当に実装してみるのが解像度も上がりオススメです。
typescript以外にも色んなプログラミング言語に対してSDKが提供され始めているので、お好きな言語で試してみてはいかがでしょうか!
ちなみに、最近GitHubの公式MCP Serverがリリースされたので、今回実装したものを自前で作る必要性は多分あまりないです (gistに関するtoolはまだなさそうだけど)、というオチがあります。
おわり