📚
MCPを使って160行のTypeScriptでミニコーディングエージェントを作る
- TypeScriptでMCPサーバーのtool呼び出しをする
-
MCPサーバーのtool呼び出しをLLMにトリガーしてもらう
を経てMCPを使い指示に従った操作をスクリプトで行えるようになりました。
次はFilesystem MCP Serverを利用して、特定のプロジェクトのソースコードを編集してもらいます。
指示に従って特定のディレクトリのコードを変更するので、これができると次は
- 指示の入力をSlackやGitHub Issue経由で実行する
- 結果をGitHub MCP ServerでGitHubのプルリクエストを自動で作る
- 状況をSlack MCP Serverで通知してもらう
- 以上をサーバー上でヘッドレスに動かす
というコーディングエージェントの構築につながります。
mini-coder.ts
##テスト
create-honoで作ったプロジェクトを変更していきます。
npm create hono@latest hono-app
cd hono-app
npm i @anthropic-ai/sdk @modelcontextprotocol/sdk
npm i -D tsx
export ANTHROPIC_API_KEY=sk-XXXX
npx tsx mini-coder.ts
ログインAPIを作成してください。
❯ npx tsx mini-coder.ts
Connected to server.
Initialized.
Secure MCP Filesystem Server running on stdio
Allowed directories: [ '/tmp/hono-app' ]
Assistaunt: ログインAPIを作成するために、以下のようなHonoのAPIエンドポイントを実装していきます。
まず、プロジェクトの構造を確認してみましょう。
{
toolUse: {
type: 'tool_use',
id: 'toolu_016dRtsezYpMQqa4h2W6hqKD',
name: 'list_directory',
input: { path: '.' }
}
}
Tool Result: {
content: [
{
type: 'text',
text: '[FILE] .env\n' +
'[DIR] .git\n' +
'[FILE] .gitignore\n' +
'[FILE] README.md\n' +
'[FILE] mini-coder.ts\n' +
'[DIR] node_modules\n' +
'[FILE] package-lock.json\n' +
'[FILE] package.json\n' +
'[DIR] src\n' +
'[FILE] system.ts\n' +
'[FILE] tsconfig.json\n' +
'[FILE] wrangler.toml'
}
]
}
Assistaunt: プロジェクトの構造が確認できました。ログインAPIを実装するために、以下の手順で進めていきます:
1. まず、src ディレクトリの中身を確認して、現在のルーティングやファイル構成を確認します。
2. ログイン用のルートハンドラーを作成します。
まず、src ディレクトリの内容を確認しましょう:
{
toolUse: {
type: 'tool_use',
id: 'toolu_01NXBw5rT8tQkifASpCNrXmz',
name: 'list_directory',
input: { path: 'src' }
}
}
{
toolUse: {
type: 'tool_use',
id: 'toolu_01NXBw5rT8tQkifASpCNrXmz',
name: 'list_directory',
input: { path: 'src' }
}
}
Tool Result: { content: [ { type: 'text', text: '[FILE] .env\n[FILE] index.ts' } ] }
Assistaunt: src ディレクトリの構造が確認できました。ログインAPIを実装していきましょう。まず、現在の index.ts の内容を確認します:
{
toolUse: {
type: 'tool_use',
id: 'toolu_019QbC7mW4w99zfKXdbVMUXa',
name: 'read_file',
input: { path: 'src/index.ts' }
}
}
{
toolUse: {
type: 'tool_use',
id: 'toolu_019QbC7mW4w99zfKXdbVMUXa',
name: 'read_file',
input: { path: 'src/index.ts' }
}
}
Tool Result: {
content: [
{
type: 'text',
text: "import { Hono } from 'hono'\n" +
'\n' +
'const app = new Hono()\n' +
'\n' +
"app.get('/', (c) => {\n" +
" return c.text('Hello Hono!')\n" +
'})\n' +
'\n' +
'export default app\n'
}
]
}
Assistaunt: 現在のコードの構造が確認できました。ログインAPIを実装するために、以下のような変更を行います:
1. ログイン用のPOSTエンドポインドを追加
2. 基本的な認証ロジックを実装
以下のコードを実装してみましょう:
{
toolUse: {
type: 'tool_use',
id: 'toolu_01KoPFS5XVmCwnkRbVb44WXp',
name: 'write_file',
input: {
path: 'src/index.ts',
content: "import { Hono } from 'hono'\n" +
'\n' +
'const app = new Hono()\n' +
'\n' +
'// 基本的なルートエンドポイント\n' +
"app.get('/', (c) => {\n" +
" return c.text('Hello Hono!')\n" +
'})\n' +
'\n' +
'// ログインAPI\n' +
"app.post('/login', async (c) => {\n" +
' try {\n' +
' const { email, password } = await c.req.json()\n' +
'\n' +
' // バリデーション\n' +
' if (!email || !password) {\n' +
' return c.json(\n' +
' {\n' +
' success: false,\n' +
" message: 'Email and password are required'\n" +
' },\n' +
' 400\n' +
' )\n' +
' }\n' +
'\n' +
' // ここで実際の認証ロジックを実装\n' +
' // この例では簡単な判定のみ(実際の実装ではデータベースでの検証が必要)\n' +
" if (email === 'test@example.com' && password === 'password123') {\n" +
' return c.json({\n' +
' success: true,\n' +
" token: 'dummy-jwt-token', // 実際の実装ではJWTトークンを生成\n" +
' user: {\n' +
' id: 1,\n' +
' email: email\n' +
' }\n' +
' })\n' +
' }\n' +
'\n' +
' return c.json(\n' +
' {\n' +
' success: false,\n' +
" message: 'Invalid credentials'\n" +
' },\n' +
' 401\n' +
' )\n' +
' } catch (error) {\n' +
' return c.json(\n' +
' {\n' +
' success: false,\n' +
" message: 'Internal server error'\n" +
' },\n' +
' 500\n' +
' )\n' +
' }\n' +
'})\n' +
'\n' +
'export default app'
}
}
}
{
toolUse: {
type: 'tool_use',
id: 'toolu_01KoPFS5XVmCwnkRbVb44WXp',
name: 'write_file',
input: {
path: 'src/index.ts',
content: "import { Hono } from 'hono'\n" +
'\n' +
'const app = new Hono()\n' +
'\n' +
'// 基本的なルートエンドポイント\n' +
"app.get('/', (c) => {\n" +
" return c.text('Hello Hono!')\n" +
'})\n' +
'\n' +
'// ログインAPI\n' +
"app.post('/login', async (c) => {\n" +
' try {\n' +
' const { email, password } = await c.req.json()\n' +
'\n' +
' // バリデーション\n' +
' if (!email || !password) {\n' +
' return c.json(\n' +
' {\n' +
' success: false,\n' +
" message: 'Email and password are required'\n" +
' },\n' +
' 400\n' +
' )\n' +
' }\n' +
'\n' +
' // ここで実際の認証ロジックを実装\n' +
' // この例では簡単な判定のみ(実際の実装ではデータベースでの検証が必要)\n' +
" if (email === 'test@example.com' && password === 'password123') {\n" +
' return c.json({\n' +
' success: true,\n' +
" token: 'dummy-jwt-token', // 実際の実装ではJWTトークンを生成\n" +
' user: {\n' +
' id: 1,\n' +
' email: email\n' +
' }\n' +
' })\n' +
' }\n' +
'\n' +
' return c.json(\n' +
' {\n' +
' success: false,\n' +
" message: 'Invalid credentials'\n" +
' },\n' +
' 401\n' +
' )\n' +
' } catch (error) {\n' +
' return c.json(\n' +
' {\n' +
' success: false,\n' +
" message: 'Internal server error'\n" +
' },\n' +
' 500\n' +
' )\n' +
' }\n' +
'})\n' +
'\n' +
'export default app'
}
}
}
Tool Result: {
content: [ { type: 'text', text: 'Successfully wrote to src/index.ts' } ]
}
Assistaunt: それでは、ログインAPIを以下のように実装していきます:
{
toolUse: {
type: 'tool_use',
id: 'toolu_013mkJn54vtEhBCyfKLYMYMg',
name: 'write_file',
input: {
path: 'src/index.ts',
content: "import { Hono } from 'hono'\n" +
"import { cors } from 'hono/cors'\n" +
'\n' +
'const app = new Hono()\n' +
'\n' +
'// CORSの設定\n' +
"app.use('/*', cors())\n" +
'\n' +
'// 基本的なヘルスチェックエンドポイント\n' +
"app.get('/', (c) => {\n" +
" return c.text('Hello Hono!')\n" +
'})\n' +
'\n' +
'// ログインAPI\n' +
"app.post('/login', async (c) => {\n" +
' try {\n' +
' const { email, password } = await c.req.json()\n' +
'\n' +
' // 入力値のバリデーション\n' +
' if (!email || !password) {\n' +
' return c.json({\n' +
' success: false,\n' +
" message: 'Email and password are required'\n" +
' }, 400)\n' +
' }\n' +
'\n' +
' // TODO: 実際のプロジェクトでは、以下の処理を実装する必要があります\n' +
' // 1. データベースでユーザーの検索\n' +
' // 2. パスワードの検証(ハッシュ化されたパスワードと比較)\n' +
' // 3. JWTトークンの生成\n' +
'\n' +
' // デモ用の簡易的な認証(実際の実装では使用しないでください)\n' +
" if (email === 'test@example.com' && password === 'password123') {\n" +
' return c.json({\n' +
' success: true,\n' +
" message: 'Login successful',\n" +
' user: {\n' +
' email,\n' +
" name: 'Test User'\n" +
' },\n' +
" token: 'dummy-jwt-token' // 実際の実装ではJWTトークンを生成する\n" +
' })\n' +
' }\n' +
'\n' +
' return c.json({\n' +
' success: false,\n' +
" message: 'Invalid credentials'\n" +
' }, 401)\n' +
'\n' +
' } catch (error) {\n' +
" console.error('Login error:', error)\n" +
' return c.json({\n' +
' success: false,\n' +
" message: 'Internal server error'\n" +
' }, 500)\n' +
' }\n' +
'})\n' +
'\n' +
'export default app'
}
}
}
{
toolUse: {
type: 'tool_use',
id: 'toolu_013mkJn54vtEhBCyfKLYMYMg',
name: 'write_file',
input: {
path: 'src/index.ts',
content: "import { Hono } from 'hono'\n" +
"import { cors } from 'hono/cors'\n" +
'\n' +
'const app = new Hono()\n' +
'\n' +
'// CORSの設定\n' +
"app.use('/*', cors())\n" +
'\n' +
'// 基本的なヘルスチェックエンドポイント\n' +
"app.get('/', (c) => {\n" +
" return c.text('Hello Hono!')\n" +
'})\n' +
'\n' +
'// ログインAPI\n' +
"app.post('/login', async (c) => {\n" +
' try {\n' +
' const { email, password } = await c.req.json()\n' +
'\n' +
' // 入力値のバリデーション\n' +
' if (!email || !password) {\n' +
' return c.json({\n' +
' success: false,\n' +
" message: 'Email and password are required'\n" +
' }, 400)\n' +
' }\n' +
'\n' +
' // TODO: 実際のプロジェクトでは、以下の処理を実装する必要があります\n' +
' // 1. データベースでユーザーの検索\n' +
' // 2. パスワードの検証(ハッシュ化されたパスワードと比較)\n' +
' // 3. JWTトークンの生成\n' +
'\n' +
' // デモ用の簡易的な認証(実際の実装では使用しないでください)\n' +
" if (email === 'test@example.com' && password === 'password123') {\n" +
' return c.json({\n' +
' success: true,\n' +
" message: 'Login successful',\n" +
' user: {\n' +
' email,\n' +
" name: 'Test User'\n" +
' },\n' +
" token: 'dummy-jwt-token' // 実際の実装ではJWTトークンを生成する\n" +
' })\n' +
' }\n' +
'\n' +
' return c.json({\n' +
' success: false,\n' +
" message: 'Invalid credentials'\n" +
' }, 401)\n' +
'\n' +
' } catch (error) {\n' +
" console.error('Login error:', error)\n" +
' return c.json({\n' +
' success: false,\n' +
" message: 'Internal server error'\n" +
' }, 500)\n' +
' }\n' +
'})\n' +
'\n' +
'export default app'
}
}
}
Tool Result: {
content: [ { type: 'text', text: 'Successfully wrote to src/index.ts' } ]
}
Assistaunt: では、ログインAPIの実装として以下のコードを追加します:
{
toolUse: {
type: 'tool_use',
id: 'toolu_014vaZxMKRHLjSLKCB7RKSGc',
name: 'write_file',
input: {
path: 'src/index.ts',
content: "import { Hono } from 'hono'\n" +
'\n' +
'interface LoginBody {\n' +
' email: string\n' +
' password: string\n' +
'}\n' +
'\n' +
'const app = new Hono()\n' +
'\n' +
"app.get('/', (c) => {\n" +
" return c.text('Hello Hono!')\n" +
'})\n' +
'\n' +
"app.post('/login', async (c) => {\n" +
' const { email, password } = await c.req.json<LoginBody>()\n' +
'\n' +
' // TODO: 実際のプロジェクトでは、データベースでの認証やパスワードのハッシュ化を実装する\n' +
" if (email === 'test@example.com' && password === 'password123') {\n" +
' return c.json({\n' +
' success: true,\n' +
" token: 'dummy-jwt-token', // TODO: 実際のプロジェクトではJWTトークンを生成する\n" +
' user: {\n' +
' id: 1,\n' +
' email\n' +
' }\n' +
' }, 200)\n' +
' }\n' +
'\n' +
' return c.json({\n' +
' success: false,\n' +
" message: 'Invalid email or password'\n" +
' }, 401)\n' +
'})\n' +
'\n' +
'export default app'
}
}
}
{
toolUse: {
type: 'tool_use',
id: 'toolu_014vaZxMKRHLjSLKCB7RKSGc',
name: 'write_file',
input: {
path: 'src/index.ts',
content: "import { Hono } from 'hono'\n" +
'\n' +
'interface LoginBody {\n' +
' email: string\n' +
' password: string\n' +
'}\n' +
'\n' +
'const app = new Hono()\n' +
'\n' +
"app.get('/', (c) => {\n" +
" return c.text('Hello Hono!')\n" +
'})\n' +
'\n' +
"app.post('/login', async (c) => {\n" +
' const { email, password } = await c.req.json<LoginBody>()\n' +
'\n' +
' // TODO: 実際のプロジェクトでは、データベースでの認証やパスワードのハッシュ化を実装する\n' +
" if (email === 'test@example.com' && password === 'password123') {\n" +
' return c.json({\n' +
' success: true,\n' +
" token: 'dummy-jwt-token', // TODO: 実際のプロジェクトではJWTトークンを生成する\n" +
' user: {\n' +
' id: 1,\n' +
' email\n' +
' }\n' +
' }, 200)\n' +
' }\n' +
'\n' +
' return c.json({\n' +
' success: false,\n' +
" message: 'Invalid email or password'\n" +
' }, 401)\n' +
'})\n' +
'\n' +
'export default app'
}
}
}
Tool Result: {
content: [ { type: 'text', text: 'Successfully wrote to src/index.ts' } ]
}
変更されました。
Discussion