コード基盤・技術選定編
前回の記事では、Slack・Redmine・GitHubに分散している開発情報をLLMで横断的に扱う「開発アシスタント」の構想について整理しました。
今回から、実際にOSSとして実装を始めていきます。
最初に決めるのは、AIのプロンプトやWeb画面ではありません。
その前に、どのようなコード基盤で作るのかを決めます。
今回の記事では、
- 使用する言語
- MCP Server
- MCP SDK
- DB
- バリデーション
- 外部サービスとの連携方法
- 認証情報の管理
- ディレクトリ構成
など、今後の実装の土台となる部分を決めていきます。
1. 今回作るもの
今回の開発アシスタントは、最終的には以下のような構成を目指します。
┌──────────────┐
│ User │
└──────┬───────┘
│
▼
┌──────────────┐
│ Agent │
│ + LLM │
└──────┬───────┘
│
▼
┌──────────────┐
│ MCP Client │
└──────┬───────┘
│
tools/list
tools/call
│
▼
┌────────────────────────┐
│ MCP Server │
└───────────┬────────────┘
│
┌──────────────┼──────────────┐
▼ ▼ ▼
┌─────────┐ ┌─────────┐ ┌─────────┐
│ Redmine │ │ GitHub │ │ Slack │
│Integration│ │Integration│ │Integration│
└────┬────┘ └────┬────┘ └────┬────┘
│ │ │
▼ ▼ ▼
Redmine GitHub Slack
今回の記事では、この中でも特に
MCP Server
Integration
Database
Credential
のコード基盤を作ることを目的にします。
RedmineやGitHubなどの具体的なAPI連携は次の記事以降で実装します。
2. 言語はTypeScript
まず、バックエンドの言語にはTypeScriptを採用します。
候補としては、
- TypeScript
- Python
- C#
などが考えられます。
今回はTypeScriptを選択しました。
理由
一番大きいのは、今回のシステムがかなり「API・外部サービス連携寄り」だからです。
Redmine、GitHub、SlackなどのAPIを呼び出し、
API
↓
データ変換
↓
LLM
↓
Tool Call
↓
API
という処理を大量に行うことになります。
TypeScriptはこのようなWeb API中心のアプリケーションを作りやすく、さらにMCP周辺のエコシステムも利用できます。
また、将来的にWeb UIを作る場合にもTypeScriptをそのまま利用できます。
Backend
TypeScript
Frontend
TypeScript
とできるのもメリットです。
3. Node.js
TypeScriptの実行環境にはNode.jsを使用します。
今回の用途では、
- HTTP Server
- MCP Server
- API Client
- DB Client
- AI SDK
- WebSocket
- CLI
などを扱う可能性があります。
Node.jsはこれらとの相性がよく、TypeScriptによる開発環境も整っています。
そのため、
TypeScript
+
Node.js
を基本的な実行環境とします。
4. MCPを中心にする
今回のシステムで重要になるのが**MCP(Model Context Protocol)**です。
単純にLLMからRedmineやGitHubのAPIを直接呼び出す構成にはしません。
代わりに、
LLM / Agent
│
▼
MCP Client
│
▼
MCP Server
│
├── Redmine
├── GitHub
└── Slack
という構成にします。
MCPを利用することで、AIから利用できる機能を「Tool」として明確に分離できます。
例えばRedmineなら、
issue.get
issue.search
issue.create
issue.update
GitHubなら、
repository.get
issue.search
pullRequest.get
pullRequest.list
Slackなら、
channel.get
message.search
message.get
といったToolとして公開できます。
5. MCP Serverは公式SDKを利用する
MCP Serverは、独自プロトコルを作らず、MCPのTypeScript SDKを利用します。
ここは重要なポイントです。
以前は、
AIから呼び出すToolを独自のJSON形式で定義する
という方法も考えられます。
しかし今回はMCPそのものを利用します。
MCPでは、ClientがServerに対してtools/listを呼び出すことで、Serverが提供しているToolの情報を取得できます。
つまり、
MCP Server
│
│ tools/list
▼
MCP Client
│
▼
Tool definitions
│
▼
LLM
という流れになります。
これによって、Tool定義を別のファイルにコピーして管理する必要がありません。
6. Tool定義はMCP ServerをSource of Truthにする
ここで一つ設計上の判断をしました。
当初は、
tool.yml
のようなファイルを作り、そこからMCP Toolの定義を生成する方法も考えました。
しかし、現時点では採用しません。
理由は単純で、MCP Server自身がTool定義を公開できるからです。
例えば、
server.tool(
"sample.add",
"Add two numbers",
{
a: z.number(),
b: z.number(),
},
async ({ a, b }) => {
return {
content: [
{
type: "text",
text: String(a + b),
},
],
};
},
);
と登録すれば、MCP Clientからtools/listでこのToolを取得できます。
そのため、
Tool registration
↓
MCP Server
↓
tools/list
をTool定義の基本的な流れとします。
余計なSSoTを増やさず、まずはMCPの標準機能に乗ることにしました。
7. Zodを使う
Toolの入力値のバリデーションにはZodを利用します。
例えば、
{
a: z.number(),
b: z.number(),
}
と定義できます。
ここで注意したいのは、
ZodがSSoTなのか?
という点です。
厳密には、今回の設計ではZodそのものを独立したSSoTとは考えません。
MCP Serverに登録したTool定義がMCP上のTool契約になります。
Zodは、
- 入力値の型を定義する
- 実行時にバリデーションする
- ToolのInput Schemaを表現する
ために利用します。
つまり、
MCP Tool
└── Input Schema
└── Zod
という関係です。
8. DBにはPostgreSQL + Prisma
アプリケーションの永続データにはPostgreSQLを使用します。
そしてORMにはPrismaを採用します。
Application
│
▼
Prisma
│
▼
PostgreSQL
今回DBで管理する予定なのは、例えば以下です。
User
Workspace
Integration
Credential
将来的には、
Agent
Conversation
Task
Execution
Approval
なども追加する可能性があります。
9. Prisma SchemaをDBモデルのSSoTにする
DBについてはPrisma Schemaを中心に管理します。
例えば、
model User {
id String @id @default(cuid())
email String @unique
createdAt DateTime @default(now())
}
のように定義します。
ここから、
- DB migration
- Prisma Client
- TypeScriptの型
を生成できます。
そのため、
schema.prisma
│
├── Migration
└── Prisma Client
という形になります。
ToolについてはMCP ServerのTool登録を中心にし、DBについてはPrisma Schemaを中心にする。
それぞれの領域で自然なSSoTを使い分けます。
10. 外部サービスはIntegrationとして分離する
今回の設計で特に重要なのがIntegrationです。
Redmine、GitHub、SlackをMCP Serverの中に直接書いてしまうと、
MCP Server
├── Redmine処理
├── GitHub処理
├── Slack処理
├── Credential処理
├── Agent処理
└── ...
となり、どんどん巨大化していきます。
そこで、外部サービスをIntegrationとして分離します。
Integrations
├── Redmine
├── GitHub
└── Slack
それぞれが、
- API Client
- 認証
- データ変換
- MCP Tool登録
などを担当します。
11. SDKを利用する
外部サービスとの通信には、基本的に各サービスのSDKを利用します。
例えば、
Redmine
↓
Redmine SDK / HTTP Client
GitHub
↓
GitHub SDK
Slack
↓
Slack SDK
という形です。
OpenAPIから全API Clientを生成する方法もありますが、今回はまずSDKを優先します。
理由は、単純なHTTP Clientを生成することが目的ではなく、
- 認証
- Pagination
- API固有の型
- API固有のエラー
- データ変換
なども扱う必要があるためです。
必要になれば、後からOpenAPIベースのClient生成を検討します。
12. CredentialStoreを抽象化する
もう一つ重要なのが認証情報です。
Redmine API KeyやGitHub Token、Slack Tokenなどをどこに保存するかという問題があります。
これをIntegration側に直接書かせないようにします。
例えば、
interface CredentialStore {
get(
userId: string,
integrationId: string,
): Promise<Credential | null>;
save(
userId: string,
integrationId: string,
credential: Credential,
): Promise<void>;
}
のような抽象を用意します。
Integration側は、
「GitHub Tokenが欲しい」
と要求するだけにします。
どこに保存されているかは知りません。
13. 最初はDB実装で十分
CredentialStoreを抽象化するからといって、最初からAWS Secrets ManagerやVaultなどを導入するわけではありません。
まずはPostgreSQLに暗号化して保存します。
CredentialStore
│
▼
DatabaseCredentialStore
│
▼
PostgreSQL
暗号化にはNode.jsのcryptoなどを利用します。
重要なのは、
Integration
↓
CredentialStore
↓
保存先
という依存関係にすることです。
将来的に、
DatabaseCredentialStore
AwsSecretsManagerCredentialStore
AzureKeyVaultCredentialStore
VaultCredentialStore
などに差し替えられるようにします。
14. IntegrationはCredentialの保存方法を知らない
例えばGitHub Integrationがあるとします。
悪い例は、
const token = process.env.GITHUB_TOKEN;
のようにIntegration自身が環境変数を直接読むことです。
今回作りたいのは複数ユーザーが利用するアプリケーションです。
そのため、
User A → GitHub Token A
User B → GitHub Token B
という状態を扱える必要があります。
そこで、
GitHub Integration
│
▼
CredentialStore
│
▼
User-specific credential
とします。
これによってIntegrationは、
「誰のCredentialなのか」
という情報をもとに必要な認証情報を取得できます。
15. Workspaceという単位を作る
もう一つ、今後重要になる概念がWorkspaceです。
例えば、
Workspace: Project A
に対して、
Redmine
Project #123
GitHub
organization/repository-a
Slack
#project-a
を紐付けます。
イメージとしては、
Workspace
├── Redmine Project
├── GitHub Repository
└── Slack Channel
です。
これによってAIは、
「Project Aについて調べて」
という依頼を受けたとき、
Redmine
↓
問い合わせ・チケット
GitHub
↓
コード・Issue・PR
Slack
↓
開発者間の会話
を横断して調査できるようになります。
Workspaceについては、この後の実装記事で詳しく扱います。
16. 現時点のディレクトリ構成
ここまでの設計を踏まえて、最初のディレクトリ構成を以下のようにします。
src/
├── agent/
│
├── mcp/
│ ├── server.ts
│ └── tools/
│
├── integrations/
│ ├── redmine/
│ │ ├── client.ts
│ │ └── tools.ts
│ │
│ ├── github/
│ │ ├── client.ts
│ │ └── tools.ts
│ │
│ └── slack/
│ ├── client.ts
│ └── tools.ts
│
├── credentials/
│ ├── credential-store.ts
│ └── database-credential-store.ts
│
├── workspace/
│
├── db/
│
├── config/
│
└── index.ts
まだ実装しないディレクトリもあります。
最初は、
mcp/
credentials/
db/
あたりから作っていきます。
17. 最初のMCP Server
まずはMCP Serverを起動できる状態にします。
最初からRedmineを実装するのではなく、単純なToolを一つ作ります。
例えば、
sample.add
です。
sample.add
a: number
b: number
↓
a + b
これだけです。
一見すると意味のないToolですが、最初の確認としては非常に重要です。
18. 動作確認
今回確認したいのは、
MCP Client
│
│ tools/list
▼
MCP Server
│
▼
sample.add
そして、
MCP Client
│
│ tools/call
│
│ a = 1
│ b = 2
▼
MCP Server
│
▼
3
という一連の流れです。
MCP Inspectorなどを利用して確認します。
ここまで動けば、
「MCP ServerをAIから利用できる基盤ができた」
と言えます。
19. 最終的にはLLMがToolを選択する
ここでMCP ServerとAI Agentの関係も整理しておきます。
MCP Serverが自分で、
「このToolを使おう」
と判断するわけではありません。
流れは、
① MCP Client
│
│ tools/list
▼
② MCP Server
│
▼
Tool一覧
│
▼
③ Agent / LLM
│
│ 「このToolを使おう」
▼
④ MCP Client
│
│ tools/call
▼
⑤ MCP Server
│
▼
⑥ Tool実行
です。
つまりMCP Serverは、
AIが利用できる「手」を提供する役割
と考えると分かりやすいです。
AIが頭脳、MCP Serverが手足というイメージです。
20. 今回の技術選定まとめ
ここまでの選定をまとめます。
| 項目 | 採用 |
|---|---|
| 言語 | TypeScript |
| Runtime | Node.js |
| AI Tool Protocol | MCP |
| MCP SDK | TypeScript MCP SDK |
| Validation | Zod |
| Database | PostgreSQL |
| ORM | Prisma |
| 外部API | 各サービスSDK / HTTP Client |
| Credential管理 | CredentialStore |
| 初期Credential保存先 | PostgreSQL |
| AI Model | 抽象化して切り替え可能にする |
| UI | 後で実装 |
| Agent | 後で実装 |
特に重要なのは、
TypeScript
│
├── MCP Server
│
├── Integration
│
├── CredentialStore
│
├── Workspace
│
└── Agent
という責務分離です。
21. なぜ最初から全部作らないのか
今回、あえてRedmine・GitHub・Slackをまだ実装していません。
理由は、最初から全部実装すると、
MCP
Redmine
GitHub
Slack
DB
認証
Agent
LLM
UI
が同時に絡み始めるからです。
そうすると、
何が原因で動かないのか分からない
という状態になりやすいです。
まず、
MCP Server
↓
sample.add
↓
MCP Client
という最小構成を動かします。
そこから、
Redmine
↓
MCP Tool
を追加します。
次に、
GitHub
↓
MCP Tool
を追加。
その後、
Slack
↓
MCP Tool
を追加します。
この順番で一つずつ積み上げていきます。
22. 次回はRedmine Integration
今回でコード基盤の方向性が決まりました。
次回はいよいよ最初のIntegrationを実装します。
対象はRedmineです。
まずは読み取り専用から始めます。
例えば、
issue.get
issue.search
project.get
などのToolを作ります。
最終的には、
LLM
↓
MCP Client
↓
MCP Server
↓
Redmine Integration
↓
Redmine API
というところまで実際に動かします。
そして、
「Redmineの問い合わせをAIが取得できる」
状態を作ります。
ここから、構想していた「開発アシスタント」が少しずつ実体になっていきます。
まとめ
今回は、開発アシスタントを実装するためのコード基盤と技術選定を行いました。
ポイントは以下です。
- バックエンドはTypeScript + Node.js
- AIとのTool連携にはMCPを利用
- MCP Serverは公式SDKを利用
- Tool定義はMCP Serverの登録を中心にする
- ZodでTool入力をバリデーションする
- DBはPostgreSQL + Prisma
- Redmine/GitHub/SlackはIntegrationとして分離する
- 外部サービスとの通信にはSDKを優先する
- CredentialはCredentialStoreとして抽象化する
- WorkspaceでRedmine/GitHub/Slackを関連付ける
- 最初は最小のMCP Serverから始める
- 実装を進めながら設計を検証していく
特に今回は、最初から完璧なアーキテクチャを作るのではなく、
「実際に動かしながら、必要な抽象化だけ追加する」
という方針で進めます。
次回は、実際にRedmine Integrationを実装していきます。