-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy path.information
More file actions
311 lines (271 loc) · 13.1 KB
/
.information
File metadata and controls
311 lines (271 loc) · 13.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
node.jsアプリケーションで簡易MCP クライアント/サーバーを作成する
はじめに
巷で話題のMCPについて、Claude DesktopやCursor, ClineなどをMCPクライアントとして紹介されている記事は多いものの、node.jsアプリケーションに組み込む方法は紹介されていないように感じたので、自作のアプリケーションに組み込む方法を紹介します。
公式ドキュメントやその他ブログ等の情報である程度MCPの情報をキャッチアップし終わり、そろそろ実装してみようかなといった人の参考になれば嬉しいなーと思っています。
記事を読むのが面倒な方は以下で実装を公開しています。 https://github.com/nyankiti/genai-lab/blob/main/src/mcpApp.ts
そもそもMCPとは
Introduction - Model Context Protocol
Get started with the Model Context Protocol (MCP)
modelcontextprotocol.io
LLMに外部データソースを組み込むためのプロトコルです。 外部データソースとは、最新の天気やローカルのファイルシステム、自社のDBなど様々です。MCPで統一されるフォーマットに従って、AI Agent的に動くLLMに追加のアクションを提供するものとなります。 できることとしては、openAIのfunction-callingとほとんど同じと感じています。function-callingに比較して実装の自由度が高くモデルに依存しないものとなっているので今後のAI Agentへの外部データソース組み込みのスタンダードになっていくのでは?と注目しています。
本記事ではnode.jsを用いて簡易的なMCP ClientとMCP Serverを作成方法を紹介します。
実装したもの
github上のあるリポジトリの直近のマージされたPRの情報を取得するツールを要するMCPサーバー
プロンプトを受け取り、必要に応じて上記 MCPサーバーが提供するツールを利用しながら回答するAI
AIクライアントにはgroq-sdk、モデルはllama-3.3-70b-versatileで利用
MCPサーバー
mcp-server.ts
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { GithubClient } from 'libs/github-clinet';
import { setupPromptHandlers } from './prompts';
import { setupToolHandlers } from './tools';
const mcpServer = new Server(
{
name: 'my-mcp-server',
version: '1.0.0',
},
{
capabilities: {
prompts: {},
tools: {},
},
},
);
const githubClinet = new GithubClient();
console.error('my MCP Server starting...');
setupToolHandlers(mcpServer, githubClinet);
console.error('my MCP Server started');
async function runServer() {
const transport = new StdioServerTransport();
await mcpServer.connect(transport);
console.error('my MCP Server running on stdio');
}
runServer().catch((error) => {
console.error('Fatal error in main():', error);
process.exit(1);
});
tools.ts
import type { Server } from '@modelcontextprotocol/sdk/server/index.js';
import {
CallToolRequestSchema,
ListToolsRequestSchema,
type Tool,
} from '@modelcontextprotocol/sdk/types.js';
import type { GithubClient } from 'libs/github-clinet';
// Toolsの定義
const TOOLS: Record<string, Tool> = {
'github-repo-merged-PRs-last-week': {
name: 'github-repo-merged-PRs-last-week',
description:
'Retrieves detailed information about pull requests that were merged within the last week in the specified GitHub repository (owner/name). This method uses GitHub API v4 (GraphQL) to fetch data, including PR title, author, merge date, number of changed files, additions, deletions, and review status.',
inputSchema: {
type: 'object',
properties: {
owner: {
type: 'string',
},
name: {
type: 'string',
},
},
required: ['owner', 'name'],
},
},
};
export const setupToolHandlers = (mcpServer: Server, githubClient: GithubClient) => {
mcpServer.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: Object.values(TOOLS),
}));
mcpServer.setRequestHandler(CallToolRequestSchema, async (request) => {
const tool = TOOLS[request.params.name];
if (!tool) {
throw new Error(`Tool not found: ${request.params.name}`);
}
// TOOLSにて定義したtoolごとに具体的な処理を実装する
if (request.params.name === 'github-repo-merged-PRs-last-week') {
const owner = request.params.arguments?.owner as string;
const name = request.params.arguments?.name as string;
console.log('owner:', owner);
console.log('name:', name);
const repositoryPullRequestsResult = await githubClient.getMergedPRsLastWeek(owner, name, 3);
return {
content: [
{
type: 'text',
text: JSON.stringify(repositoryPullRequestsResult),
},
],
};
}
throw new Error('Tool implementation not found');
});
};
AIクライアント(MCPクライアント)
messages配列の中に、MCPサーバーを呼び出すためのrole:assistantなメッセージ, MCPサーバーからのレスポンスを追加して文脈を増やしながら最終的な回答を生成するという流れで実装しています。
mcp-client.ts
import path from 'node:path';
import { Client as MCPClient } from '@modelcontextprotocol/sdk/client/index.js';
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
import dotenv from 'dotenv';
import Groq from 'groq-sdk';
import type {
ChatCompletionMessageParam as GroqChatCompletionMessageParam,
ChatCompletionTool as GroqChatCompletionTool,
} from 'groq-sdk/resources/chat/completions';
import OpenAI from 'openai';
import type { ChatCompletionMessageParam, ChatCompletionTool } from 'openai/resources';
import { z } from 'zod';
dotenv.config();
const myMCPServerScriptPath = path.join(process.cwd(), 'src', 'libs', 'mcp-server', 'index.ts');
const MODEL_NAME = 'llama-3.3-70b-versatile';
const MAX_TOKENS = 1000;
export class MyMCPClient {
private mcpClient: MCPClient;
private openaiClient: OpenAI;
private groqClient: Groq;
private transport: StdioClientTransport;
private availableTools: Awaited<ReturnType<MCPClient['listTools']>>['tools'] = [];
constructor() {
this.mcpClient = new MCPClient({
name: 'my-mcp-client',
version: '1.0.0',
});
this.openaiClient = new OpenAI({
apiKey: process.env.GROQ_API_KEY,
baseURL: 'https://api.groq.com/openai/v1',
});
this.groqClient = new Groq({
apiKey: process.env.GROQ_API_KEY,
});
const githubPersonalAccessToken = process.env.GITHUB_PERSONAL_ACCESS_TOKEN;
if (!githubPersonalAccessToken) throw new Error('GITHUB_PERSONAL_ACCESS_TOKEN is not set');
this.transport = new StdioClientTransport({
command: 'tsx',
args: [myMCPServerScriptPath],
env: {
GITHUB_PERSONAL_ACCESS_TOKEN: githubPersonalAccessToken,
...(process.env as Record<string, string>),
},
});
}
async connectToServer(): Promise<void> {
await this.mcpClient.connect(this.transport);
console.log('Connected to MCP server');
const listToolsResponse = await this.mcpClient.listTools();
this.availableTools = listToolsResponse.tools;
}
async processQuery(query: string): Promise<string | null | undefined> {
const messages: Array<GroqChatCompletionMessageParam & { name?: string }> = [
{ role: 'user', content: query },
];
const availableTools: Array<GroqChatCompletionTool> = this.availableTools.map((tool) => ({
type: 'function',
function: {
name: tool.name,
description: tool.description,
parameters: tool.inputSchema,
},
}));
const response = await this.groqClient.chat.completions.create({
model: MODEL_NAME,
messages: messages,
tools: availableTools,
tool_choice: 'required',
max_completion_tokens: 4096,
});
const responseMessage = response.choices[0].message;
const toolCalls = responseMessage.tool_calls;
if (toolCalls) {
messages.push(responseMessage);
for (const toolCall of toolCalls.slice(0, 1)) {
const toolName = toolCall.function.name;
const toolArgs = JSON.parse(toolCall.function.arguments);
const toolResult = await this.mcpClient.request(
{
method: 'tools/call',
params: {
name: toolName,
arguments: toolArgs,
},
},
z.any(),
);
// FOR DEBUG
console.log('Tool call result:', toolResult);
const toolResultContents = JSON.stringify(
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
toolResult.content.map((content: any) => {
if (content.type === 'text') return content.text;
throw Error(`content type not supported: ${content.type}, ${content}`);
}),
);
messages.push({
tool_call_id: toolCall.id,
role: 'tool',
name: toolName,
content: toolResultContents,
});
}
const secondResponse = await this.groqClient.chat.completions.create({
model: MODEL_NAME,
messages: messages,
});
return secondResponse.choices[0].message.content;
}
return responseMessage.content;
}
async close(): Promise<void> {
await this.mcpClient.close();
}
}
動作確認
'facebook/react リポジトリの直近一週間にマージされPRの内容についてmarkdown形式で要約して。どのような議論があったかも要約して。PRのリンクも参照できるようにして。'とAIに質問した場合の結果にです。
### facebook/react リポジトリの直近一週間にマージされたPRの内容について
以下のPRは、直近一週間にマージされたPRです。
* **PR \#32621: Partially revert #32588**
* このPRは、テストが適切に実行されない問題を修正するために、前のPR \#32588で行われた一部の変更を取り消しています。
* [PR \#32621](https://github.com/facebook/react/pull/32621)
* **PR \#32617: Remove Mutation Check Around commit/measureUpdateViewTransition**
* このPRは、`commit/measureUpdateViewTransition`にわざわざ変更をチェックする必要がないことを認識し、不要なチェックを削除しています。
* [PR \#32617](https://github.com/facebook/react/pull/32617)
* **PR \#32613: Move traverseFragmentInstanceChildren to internal ReactFiberTreeReflection**
* このPRは、`traverseFragmentInstanceChildren`という関数を、内部の`ReactFiberTreeReflection`モジュールに移動することで、ConfigがFiberの内部を知らなくても済むようにしています。
* [PR \#32613](https://github.com/facebook/react/pull/32613)
* **PR \#32612: Measure and apply names for the "new" phase**
* このPRは、"new"フェーズの AnimatedとSwipeで Namesの確認と適用を実装しています。
* [PR \#32612](https://github.com/facebook/react/pull/32612)
* **PR \#32599: Find Pairs and Apply View Transition Names to the Clones in the "old" Phase**
* このPRは、"old"フェーズで、AnimatedとSwipeに対して、ViewTransition Boundariesと名前の一致を確認しています。
* [PR \#32599](https://github.com/facebook/react/pull/32599)
### PRの議論について
これらのPRについては、多くの議論や確認が行われています。開発者同士で_CODE_やロジックについて意見を出し合い、各自が行った変更点について説明しています。 一部のPRでは、スクリーンショットを通じて問題点の可視化が行われ、理解を促進しています。 逆に、他のPRでは、開発者間での確認や承認が、テキストベースでのみ行われている様子があります。 このように、facebook/reactの開発では、開発者のコミュニケーションが活発に行われていることが感じられます。
以下はmessageオブジェクトの中身です。
[
{
role: 'user',
content: 'facebook/react リポジトリの直近一週間にマージされPRの内容についてmarkdown形式で要約して。どのような議論があったかも要約して。PRのリンクも参照できるようにして。'
},
{
role: 'assistant',
tool_calls: [
{
id: 'call_akw3',
type: 'function',
function: {
name: 'github-repo-merged-PRs-last-week',
arguments: '{"owner": "facebook", "name": "react"}'
}
}
]
},
{
tool_call_id: 'call_akw3',
role: 'tool',
name: 'github-repo-merged-PRs-last-week',
content: '[toolからのレスポンス]'
}
]
最後に
AI Agenet的にLLMを利用するとどうしても利用トークンが爆増しまうので、現状個人のアプリケーションに組み込むにはコストがすごいことになりそうな気がしました🥲
groqの無料枠の範囲内で効率的な情報収集や定期実行等で利用できればよいなーと思ったりしています。