📚

MCPを使って160行のTypeScriptでミニコーディングエージェントを作る

2025/01/10に公開
  1. TypeScriptでMCPサーバーのtool呼び出しをする
  2. MCPサーバーのtool呼び出しをLLMにトリガーしてもらう
    を経てMCPを使い指示に従った操作をスクリプトで行えるようになりました。
    次はFilesystem MCP Serverを利用して、特定のプロジェクトのソースコードを編集してもらいます。

指示に従って特定のディレクトリのコードを変更するので、これができると次は

  1. 指示の入力をSlackやGitHub Issue経由で実行する
  2. 結果をGitHub MCP ServerでGitHubのプルリクエストを自動で作る
  3. 状況をSlack MCP Serverで通知してもらう
  4. 以上をサーバー上でヘッドレスに動かす
    というコーディングエージェントの構築につながります。

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