サイオステクノロジー(Tech.Lab)のブログ - TECH PLAY

TECH PLAY

サイオステクノロジー(Tech.Lab)

サイオステクノロジー(Tech.Lab) の技術ブログ

672

はじめに お久です!皆さんAIにどれぐらい課金していますか?動画配信サービスより、AIサービスのほうが課金額が高くなっている龍ちゃんです。サービスごとに特色もあり、得意領域もそれぞれ異なるので、いっそのこと10万ぐらいぶっこんでしまいたいところですね。 さて!今回は、AIサービスを使うにあたって便利にしていこうというお話です。具体的な部分としては「SlackからNotebook LMに簡単にデータを取り込む方法」のプロトタイプについてです。 今回はベースのコンテンツを作成して、執筆はAIにやらせてみようかと思います。 なぜこのシステムを開発したのか? このプロトタイプを開発した背景には、AIサービスの活用における課題があります。特に、現在様々なAIサービスが登場し、それぞれの特色や得意分野が異なる中で、効率的な情報管理と連携の重要性が高まっています。 具体的な課題として、Google Docsを普段使用していないためのデータ連携の困難さや、Slackでの会話内容を効率的にまとめる必要性がありました。また、Slack AIは比較的高価である一方、既に契約済みのNotebook LMを有効活用したいという思いもありました。 そこで今回は、「SlackからNotebook LMに簡単にデータを取り込む方法」のプロトタイプ開発に着手しました。初期段階として、Slackのテキストメッセージのみを対象とし、スレッドを一つの単位として取り込む基本的な機能の実装を目指しています。 Notebook LMでは、一度紐づけたソースが更新された場合は手動でアクションを行う必要があるため完全な自動化には至っていません。ですが、Google Docsを起動することなく情報を転機できるため、AIと開発の連携をより上げることができると期待しています。 龍ちゃん あとは!この記事を読んで作りたくなったからですね! 使用技術 今回開発したシステムは、以下のような技術スタックを使用しています。まず、バックエンドフレームワークとしてNestJSを採用しました。これは既存のデプロイ環境との親和性を考慮した選択です。 @slack/web-apiライブラリ を利用することで、Slackとの連携を効率的に実装することができました。 Slack APIに関しては、主に3つの機能を活用しています。まず、Event Subscriptionsの reactions.item_added を使用してリアクションの検知を行います。次に、メッセージの取得には conversations.history でリアクション対象のメッセージを、 conversations.replies でスレッド内の返信を取得します。また、 chat.postMessage を使用してスレッドへの返信機能も実装しています。 Google Docs APIについては、サービスアカウントを利用して認証を行い、 documents.batchUpdate を使用してドキュメントへのテキスト追記を実現しています。これにより、Slackでの会話内容を自動的にGoogle Docsに記録することが可能になりました。 最終的に、Google DocsとNotebook LMを連携させることで、Slackの内容を活用してNotebook LMを効率的に使用できるようになります。 処理フロー 処理フローは以下の流れになります。 初期設定とメッセージ送信 ユーザーがSlackにメッセージを送信すると、リアクション追加の準備が整います。 Slack Events APIによる監視 バックエンドAPIでは、Slack Events APIを使用してSlackでの活動を監視します。具体的には: POST /api/slack/events エンドポイントでSubscription Eventを受信 reaction_added イベントをキャッチして処理を開始(特定のリアクションの場合は処理) メッセージ情報の取得 リアクションが追加されると、システムは以下の処理を実行します: Slack APIの GET Slack Message を呼び出し Slack APIを用いてメッセージを取得 conversations.history :リアクションがつけられたメッセージ取得 conversations.replies :リアクションをつけられたメッセージにスレッドがあればスレッドのメッセージも取得する Google Docs連携処理 取得したメッセージ情報をもとに、リアクション対応の処理を実行: Google Docs更新処理を開始 Service Accountを使用した認証で安全にアクセス ドキュメントの内容を更新 完了通知 処理完了後、Slackに結果を通知: chat.postMessage でリアクション対象への結果通知 処理結果確認のメッセージ送信 構築 ここでは、実際のシステム構築について詳しく説明していきます。プロトタイプとはいえ、実用的な機能を備えたシステムを構築することができました。以下、主要なコンポーネントごとに実装の詳細を解説していきます。部分的なコードを解説用に添付します。最終的なコードはGitHubのリポジトリとブログの最後に完成版のコードを張ります。 Google Docsの更新 Google Docsを操作するためにサービスアカウントを用いて認証を行っています。 サービスアカウントって何? サービスアカウントとは、ユーザーがログインしなくても、プログラムがGoogleのサービスにアクセスできるようにする、特別なアカウントのことです。人間でいうと「あなたは〇〇のタスクを代わりにやってくれる、もう一人の自分」のような存在です。これにより、システムが裏側で黙々と処理を進めたり、決められたタイミングで情報を記録したりといった、自動化がスムーズに実現できるようになるんです。 Google Docsへの更新処理は、主に以下のような流れで実装しています: まず、Google Cloud Platformでサービスアカウントを作成し、必要なAPIを有効化します。認証情報をJSONファイルとして取得し、これを使用してGoogle APIにアクセスします。NestJSからは、googleapisライブラリを使用してクライアントを作成し、適切なスコープ(documents、drive)を設定します。スコープとしては、以下を指定しています。 スコープ 説明 https://www.googleapis.com/auth/documents Googleドキュメント操作に必要なスコープ https://www.googleapis.com/auth/drive Googleドライブ操作に必要なスコープ サービスアカウントで割り振られたメールアドレスで操作したいGoogleドキュメントに、共有権限でドキュメントを共有します。 認証周りの処理はnest.jsの Configuration を通じて共通的に保持しています。 import { MessagingApiClient } from '@line/bot-sdk/dist/messaging-api/api'; import { Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { docs_v1, google } from 'googleapis'; @Injectable() export class EnvironmentsService { constructor(private configService: ConfigService) {} private googleDocs: docs_v1.Docs; get googleDosc() { if (this.googleDocs) return this.googleDocs; const auth = new google.auth.GoogleAuth({ credentials: { client_email: process.env.DOCS_CLIENT_EMAIL, private_key: process.env.DOCS_PRIVATE_KEY.replace(/\\n/g, '\n'), // 環境変数から読み込む場合は改行コードを修正 }, scopes: ['https://www.googleapis.com/auth/documents', 'https://www.googleapis.com/auth/drive'], // 必要なスコープ }); this.googleDocs = google.docs({ version: 'v1', auth, }); return this.googleDocs; } get GoogleDocsID(): string { return this.configService.get('DOCS_ID'); } } 実際のドキュメント操作では、 documents.batchUpdate メソッドでDocsのトップに追記する形で更新します。この際、Slackから取得したメッセージの内容を適切なフォーマットに変換して追記します。 import { Injectable } from '@nestjs/common'; import { EnvironmentsService } from 'src/config/enviroments.service'; @Injectable() export class DocsAccessService { constructor(private readonly env: EnvironmentsService) {} docs = this.env.googleDosc; async updateDoc(docId: string, text: string) { try { const response = await this.docs.documents.batchUpdate({ documentId: docId, requestBody: { requests: [ { insertText: { text: text, location: { index: 1, // Insert at the beginning of the document }, }, }, ], }, }); return response.data; } catch (error) { console.error('Error updating document:', error); throw error; } } } これにより、API経由で内容を自動的にGoogle Docsに記録し、後でNotebook LMで活用できる形式で保存することが可能になりました。 Slackにボットメッセージを送信する Slackからのメッセージ送信では、 Bot User OAuth Tokens を使用します。これには、 channels:history 、 chat:write 、 reactions:read の権限が必要です。 まずは、先ほど取得したトークンを環境変数として参照できるようにします。 import { MessagingApiClient } from '@line/bot-sdk/dist/messaging-api/api'; import { Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; @Injectable() export class EnvironmentsService { constructor(private configService: ConfigService) {} get SlackBotToken(): string { return this.configService.get('SLACK_BOT_TOKEN'); } } メッセージの送信方法は主に2種類あり、 chat.postMessage で通常のメッセージを、 chat.postEphemeral で一時的なメッセージを送信できます。Slack Botの捜査には @slack/web-api を使用しています。 import { Injectable } from '@nestjs/common'; import { ReactionAddedEvent, WebClient } from '@slack/web-api'; import { EnvironmentsService } from 'src/config/enviroments.service'; import { DocsAccessService } from 'src/utils/docs-access/docs-access.service'; @Injectable() export class SlackService { private client: WebClient; constructor( private readonly env: EnvironmentsService, private readonly docService: DocsAccessService, ) { const token = this.env.SlackBotToken; this.client = new WebClient(token); } async postMessage(channelId: string, text: string, threadTs?: string): Promise<void> { try { console.log(`Attempting to post message to channel ${channelId}${threadTs ? ` in thread ${threadTs}` : ''}`); await this.client.chat.postMessage({ channel: channelId, text: text, thread_ts: threadTs, // ここに返信したいメッセージのtsを指定 // optional: icon_emoji: ':robot_face:', // カスタムアイコンを使いたい場合 // optional: username: 'My Reaction Bot', // カスタムユーザー名を使いたい場合 }); // console.log('Message posted successfully:', result.ts); } catch (error) { console.error(`Failed to post message: ${error.message}`, error.stack); if (error.data) { console.error('Slack API error response:', error.data); } throw error; } } // あなただけに表示されています系メッセージ async postMessageEphemeral(channelId: string, text: string, threadTs: string, user: string): Promise<void> { try { console.log(`Attempting to post message to channel ${channelId}${threadTs ? ` in thread ${threadTs}` : ''}`); await this.client.chat.postEphemeral({ channel: channelId, user: user, text: text, thread_ts: threadTs, // ここに返信したいメッセージのtsを指定 // optional: icon_emoji: ':robot_face:', // カスタムアイコンを使いたい場合 // optional: username: 'My Reaction Bot', // カスタムユーザー名を使いたい場合 }); // console.log('Message posted successfully:', result.ts); } catch (error) { console.error(`Failed to post message: ${error.message}`, error.stack); if (error.data) { console.error('Slack API error response:', error.data); } throw error; } } } 二つのメソッドはほとんど同様の使い方ができます。明確な違いとしては、他のユーザーからの視認性・メッセージの修正の有無にあります。 特徴 chat.postMessage chat.postEphemeral 用途 チャンネルやスレッドに永続的なメッセージを投稿する 特定のユーザーに対してのみ一時的な(非公開の)メッセージを投稿する 可視性 そのメッセージが投稿されたチャンネルの全員に見える 指定した user のみに見え、他のチャンネルメンバーには見えない 持続性 チャンネルの履歴に残り、後から参照可能 ユーザーがSlackクライアントを再起動したり、セッションを終了したりすると消える可能性がある(Slackの保証はないが、一時的と認識すべき) 宛先指定 channel パラメータでチャンネルIDまたはユーザーID(DMの場合)を指定 channel パラメータでチャンネルID、user パラメータでメッセージを表示するユーザーID を指定 スレッド返信 thread_ts パラメータでスレッドに返信可能 thread_ts パラメータでスレッド内の特定のユーザーに返信可能 APIスコープ chat:write(通常) chat:write.public(パブリックチャンネルのみ) chat:write.private(プライベートチャンネルのみ) chat:write または chat:write.ephemeral メッセージの編集/削除 chat.update や chat.delete で後から編集・削除が可能 基本的に後から編集・削除する機能はない (表示されるかどうかはSlackクライアントに依存するため) リアクションイベントを受け付ける Events Subscriptionsを設定することで、Slackでのリアクションなどのイベントを検知できるようになります。今回のプロトタイプでは、メッセージへのリアクション追加・削除を監視するため、 reaction_added と reaction_removed イベントを使用しています。 処理としては、URL検証用リクエスト url_verification とイベントコールバック event_callback を取得する処理が割り振られています。 import { Body, Controller, Post, UseGuards } from '@nestjs/common'; import { SlackService } from './slack.service'; import { SlackBotSignatureGuard } from 'src/common/guard/slack-bot-signature/slack-bot-signature.guard'; import { SlackEvent } from '@slack/types'; @Controller('/api/slack/') export class SlackController { constructor(private readonly slackService: SlackService) {} @UseGuards(SlackBotSignatureGuard) @Post('events') async handleSlackEvents(@Body() payload): Promise<string | { challenge: string }> { // SlackのイベントがURL検証リクエストの場合は、challengeを返す // これにより、Slackがイベントサブスクリプションを確認できるようになります if (payload.type === 'url_verification') { console.log('URL Verification Request handled.'); return { challenge: payload.challenge }; } const event: SlackEvent = payload.event; // イベントのタイプがサポートされていない場合はログを出力して終了 // ここでは 'event_callback' タイプのみを処理する // 必要に応じて他のイベントタイプを追加することができます if (payload.type !== 'event_callback' || !event) { console.log('Unsupported Slack event type:', payload.type); return 'OK'; // Slackに200 OKを返却 } // イベントの処理を行う if (event.type === 'reaction_added') { // リアクションが 'notebooklm' の場合のみ処理を行う if (event.reaction == 'notebooklm') { this.slackService.updateDataSource(event); } return 'OK'; // Slackに200 OKを返却 } return 'OK'; // Slackに200 OKを返却 } } リアクションイベントの型定義は こちらのリファレンス を参照してください。リアクション追加判別後、特定のリアクションが追加されたときのみ処理を行うようにしています。リアクション追加イベントには、リアクション情報とリアクションがつけられた対象を特定するための情報(ts/channel)が含まれています。こちらを使用してメッセージを取得します。 Slack スレッドに送信されたメッセージをすべて取得 import { Injectable } from '@nestjs/common'; import { ReactionAddedEvent, WebClient } from '@slack/web-api'; import { EnvironmentsService } from 'src/config/enviroments.service'; import { DocsAccessService } from 'src/utils/docs-access/docs-access.service'; @Injectable() export class SlackService { constructor( private readonly env: EnvironmentsService, ) { const token = this.env.SlackBotToken; this.client = new WebClient(token); } async getMessage(channelId: string, ts: string): Promise<string> { try { const result = await this.client.conversations.history({ channel: channelId, latest: ts, limit: 1, inclusive: true, }); if (result.messages && result.messages.length > 0) { const message = result.messages[0]; if (message.thread_ts) { const repilesResponse = await this.client.conversations.replies({ channel: channelId, ts: message.thread_ts, }); const replies = repilesResponse.messages .map((reply) => { // TODO:BOTが返信したメッセージを除外する if (reply.bot_id) return ''; return reply.text ? reply.text : ''; }) .join('\n'); console.log('Replies retrieved successfully:', replies); return replies; } return message.text ? message.text : ''; } else { console.warn('No messages found for the given ts'); return null; } } catch (error) { console.error(`Failed to get message: ${error.message}`, error.stack); if (error.data) { console.error('Slack API error response:', error.data); } throw error; } } } Slackのメッセージ取得について、いくつかの重要な点と制限事項があります。リアクションイベント内に含まれるチャンネルIDとts(タイムスタンプ)で conversations.history を limit:1 で実行することで親スレッドを特定します。これはスレッド内にてリアクションイベントが発生しても、取得できるのは親スレッドの情報というSlack API特有の仕様です。 API仕様書はこちらになります 。 スレッドを含むメッセージを完全に取得するためには、スレッド全体を別途取得する必要があります。BOTからの自動返信もメッセージとして含まれる可能性があり、これが実際の利用者の会話の流れを把握する際に不都合を生じさせることがあります。そのため、メッセージのフィルタリングや処理方法について、慎重な設計が必要となります。今回は、 conversations.replies の結果でBOTの情報が含まれる場合はメッセージを除外しています。 API仕様書はこちらになります 。 リアクションイベントからGoogle Docsの取得までを一つの処理としてまとめる ここまで、個別の処理に切り分けて実装していました。最終的にコントローラーから処理を受け取るサービスとして一つの関数にまとめていきます。 import { Injectable } from '@nestjs/common'; import { ReactionAddedEvent, WebClient } from '@slack/web-api'; import { EnvironmentsService } from 'src/config/enviroments.service'; import { DocsAccessService } from 'src/utils/docs-access/docs-access.service'; @Injectable() export class SlackService { private client: WebClient; constructor( private readonly env: EnvironmentsService, private readonly docService: DocsAccessService, ) { const token = this.env.SlackBotToken; this.client = new WebClient(token); } async updateDataSource(event: ReactionAddedEvent): Promise<void> { const channelId = event.item.channel; const ts = event.item.ts; const message = await this.getMessage(channelId, ts); if (!message) { console.warn(`Message not found for channel ${channelId} and ts ${ts}`); } if (message != '') { const docId = this.env.GoogleDocsID; const text = `\n---\n${message}\n---\n`; await this.docService.updateDoc(docId, text); this.postMessage( channelId, `Notebook LMデータソースに追記しました: ${event.reaction} by <@${event.user}> \n\n${message}`, ts, // ここでスレッドのtsを指定 ); } else { this.postMessage( channelId, `現在テキストソースにしか対応していません。リアクションをつけたメッセージはテキストが空でした。`, ts, // ここでスレッドのtsを指定 ); } } } 今回解説した関数を利用して、イベントを受け取りGoogle Docsの更新・SlackへのBotによるメッセージ送信までの実装が完了しました。 エラーハンドリングと署名検証 エラーハンドリングと今後の課題について詳しく見ていきましょう。 Slackの署名検証 公式でも言及されていますが、アクセスがSlackからのものであることを確約する 必要があります。nest.jsではGuardsという機能を用いて、コントローラーにアクセスする前に事前に検証をすることができます。処理自体は公式の情報をもとに構築してあります。 必要になるのは、Slack Developerで取得することができる Signing Secret です。 import { CanActivate, ExecutionContext, Injectable, RawBodyRequest, UnauthorizedException } from '@nestjs/common'; import { createHmac, timingSafeEqual } from 'crypto'; import { Observable } from 'rxjs'; import { EnvironmentsService } from 'src/config/enviroments.service'; @Injectable() export class SlackBotSignatureGuard implements CanActivate { constructor(private readonly env: EnvironmentsService) {} private readonly MAX_TIMESTAMP_AGE_SECONDS = 300; // 5分 (リプレイアタック防止のため) canActivate(context: ExecutionContext): boolean | Promise<boolean> | Observable<boolean> { const request = context.switchToHttp().getRequest<RawBodyRequest<Request>>(); const slackSignature = request.headers['x-slack-signature'] as string; const slackTimestamp = request.headers['x-slack-request-timestamp'] as string; const rawBody = (request as any).rawBody; // main.ts で bodyParser を設定して取得 if (!slackSignature || !slackTimestamp || !rawBody) { console.error('署名検証が失敗しました: 必要なヘッダーまたはボディが不足しています。'); throw new UnauthorizedException('署名検証が失敗しました: 必要なヘッダーまたはボディが不足しています。'); } // リプレイアタック const timestamp = parseInt(slackTimestamp, 10); const currentTime = Math.floor(Date.now() / 1000); // Unixタイムスタンプ (秒) if (Math.abs(currentTime - timestamp) > this.MAX_TIMESTAMP_AGE_SECONDS) { console.warn( // 日本語にしてエラーメッセージをわかりやすくする `Slackリクエストのタイムスタンプが古すぎるか、未来のものです。タイムスタンプ: ${timestamp}, 現在: ${currentTime}`, ); throw new UnauthorizedException('Slackリクエストのタイムスタンプが古すぎるか、未来のものです。'); } // Slackの署名検証 const baseString = `v0:${timestamp}:${rawBody.toString()}`; const hmac = createHmac('sha256', this.env.SlackBotSigningSecret); hmac.update(baseString); const computedSignature = `v0=${hmac.digest('hex')}`; if (!timingSafeEqual(Buffer.from(computedSignature), Buffer.from(slackSignature))) { console.warn('Slackの署名が無効です。'); throw new UnauthorizedException('Slackの署名が無効です。'); } return true; } } エラーハンドリング プロトタイプとしての現在の実装では、基本的なエラーハンドリングのみを実装していますが、本番環境での運用を想定した場合、以下のような設計が必要になりそうです。 Slack API関連のエラー処理: レート制限への対応とリトライロジックの実装 API接続タイムアウトの適切な処理 チャンネルアクセス権限エラーの処理 Google Docs API関連のエラー処理: 認証エラーの適切な処理とトークンリフレッシュ ドキュメント編集権限エラーのハンドリング API制限到達時の待機ロジック実装 システム全般のエラー処理: エラーログの構造化と保存 重要なエラーの管理者への通知システム システムの状態回復メカニズムの実装 これらのエラーハンドリングを実装することで、システムの安定性と信頼性が大幅に向上します。また、運用面でのトラブルシューティングも容易になります。 今後の展望 今回のプロトタイプ開発を通じて、SlackからNotebook LMへのデータ連携という基本的な仕組みは構築できました。しかし、これはあくまで第一歩であり、より実用的で価値のあるシステムに発展させるための道筋がいくつか見えてきました。 機能拡張の方向性 ファイル形式の対応拡大 現在はテキストメッセージのみの対応ですが、Slackでは画像、PDF、スプレッドシートなど様々なファイルが共有されます。特に、画像からのOCR処理やPDFの内容抽出機能を追加することで、より包括的な情報収集が可能になります。Google Cloud VisionやDocument AIとの連携により、これらの実装は十分現実的です。 リアクション種別による分類機能 現在は単一のリアクション( :notebooklm: )のみに対応していますが、複数のリアクションを使い分けることで、情報を自動分類できるようになります。例えば: :important: → 重要な情報として優先度高でマーク :todo: → タスクリストとして別ドキュメントに記録 :knowledge: → ナレッジベース用ドキュメントに整理 この仕組みにより、単なるデータ収集から、目的別の情報整理システムへと発展させることができます。 AI要約機能の統合 現在はSlackのメッセージをそのままGoogle Docsに転記していますが、長いスレッドや議論については、Azure OpenAIやAnthropic APIを活用した要約機能を追加したいと考えています。これにより、本質的な内容のみを抽出してNotebook LMに渡すことが可能になり、より効率的な情報活用が実現できます。 システム改善の取り組み パフォーマンスとスケーラビリティの向上 現在の同期処理から非同期処理への移行は必須です。Redis Queueやbull.jsを使用したジョブキューイングシステムを導入し、大量のメッセージ処理にも対応できる構成に変更予定です。また、Google Docsの容量制限を考慮し、定期的なドキュメント分割機能も検討しています。 ユーザーエクスペリエンスの改善 現在のシンプルなBot返信から、より詳細なフィードバック機能への拡張を計画しています。処理状況の可視化、エラー時の分かりやすい説明、さらにはSlashコマンドを使った手動操作機能なども追加したいところです。 監視とメンテナンス機能 本格運用を見据えて、システムヘルスチェック機能やログ分析ダッシュボードの構築も重要です。特に、API使用量の監視やエラー傾向の分析機能により、安定したサービス提供を目指します。 技術的挑戦 Notebook LM APIの活用 現在はGoogle Docsを経由した間接的な連携ですが、今後Notebook LM APIが公開された際には、より直接的な統合を検討したいと思います。これにより、リアルタイムでの質問応答機能や、自動的なインサイト生成なども実現可能になるかもしれません。 マルチプラットフォーム対応 Slack以外のコミュニケーションツール(Microsoft Teams、Discord、Mattermost等)への対応も視野に入れています。共通のインターフェースを設計することで、組織の使用ツールに関係なく同様の価値を提供できるシステムを目指します。 セキュリティとコンプライアンス強化 企業利用を考慮し、データの暗号化、アクセス権限の細分化、監査ログの充実などを進める必要があります。特に、個人情報や機密情報を含む可能性のあるSlackメッセージの取り扱いについては、慎重な設計が求められます。 おわりに 今回開発したプロトタイプは、AIを活用した情報管理の可能性を示す小さな一歩でした。しかし、ここから得られた知見と技術基盤を活かし、より実用的で価値のあるシステムへと発展させていきたいと考えています。 特に、現在のAIブームの中で、単にAIツールを使うだけでなく、既存のワークフローにAIを自然に統合する仕組みの重要性を改めて感じました。SlackのようなコミュニケーションツールとNotebook LMのような分析ツールを橋渡しすることで、日常的な業務の中で自然にナレッジが蓄積され、活用される環境を作ることができそうです。 今後も継続的に改善を重ね、最終的には「気がついたら素晴らしいナレッジベースができていた」と感じられるような、透明で価値のあるシステムを目指していきます。皆さんも、ぜひ様々なAIサービスを組み合わせて、独自の価値を生み出すシステム構築にチャレンジしてみてください! 弊社ではAI活用頑張ってますので、こちらも併せてチェック!! 2025-05-31 PRレビューを自動化しよう!GitHub Copilot × システムプロンプトの基本 2025-05-30 GitHub Copilotをチーム開発で使いこなす!システムプロンプト設定方法 ご覧いただきありがとうございます! この投稿はお役に立ちましたか? 役に立った 役に立たなかった 0人がこの投稿は役に立ったと言っています。 The post Notebook LMへのデータ収集をSlack Botで効率化する開発 with Google Docs first appeared on SIOS Tech. Lab .
先日、久しぶりにGoogle App Script(GAS)で作成したコードのメンテナンスのために、GASのエディタを開いた龍ちゃんです。既存のコードが動いているからと言って、自分がメンテしていない外部APIを使用していると、知らないうちに更新が入る可能性もありますよね。やはり失敗を検知する仕組みは必要ですね。 今回は「GASの実行可能API」についてのお話です。 GASの公開方法としては「ウェブアプリ」「実行可能API」の二つがあります。 実行可能APIとしてデプロイされたアプリを実行するためには、Google OAuth2.0を利用して認可を行い、アクセストークンを取得する必要があります。 実際にアクセストークンを取得する流れをステップバイステップで解説していきます。 前提条件 バックエンドはnest.jsを使用して作成しています。具体的なコードに関しては、ブログの最後に記載しておきます。 デプロイ先 想定している環境としては、クライアントをAzure Static Web App(SWA)、バックエンドをAzure App Seriveで構築しています。SWAとApp ServiceはAPIリンク(Bring Your Own Functions方式)で接続しています。APIリンクの場合は、バックエンドを /api にルーティングして同一ホストとして処理することができます。 ソースコードはGitに上がっています。 リポジトリ 説明 https://github.com/Ryunosuke-Tanaka-sti/2025-line-liff-frontend フロントエンド https://github.com/Ryunosuke-Tanaka-sti/2024-line-liff-app-backend/tree/main バックエンド 実行可能APIを作成する(Google App Script) 検証のために以下の簡易的な関数を作成してデプロイをします。 String : “Hello World!!”を返答する関数 Google Sheetに書き込まれている情報をすべて取得・返答する Google Sheetに一行追加する 実行可能APIとしてデプロイする前にGASのエディタ上で実行しておくと、デバッグすることができます。 Health Check Function:Hello Worldを返答する関数 非常に単純な関数です。 function healthCheckFunction() { return "Hello World!"; } getSheetAllData:Google Sheetに書き込まれている情報をすべて返答 GASからGoogle Sheetを操作して、Sheet内にあるすべての情報を取得する関数になります。GASからGoogle Sheetを操作する方法に関しては、こちらの「 Google Apps Script スプレッドシート編 初心者向け 」でまとめています。 function getSheetAllData() { // SHEET_ID・SHEET_NAMEの情報を更新してください const file = SpreadsheetApp.openById("SHEET_ID") const sheet = file.getSheetByName("SHEET_NAME") const lastRow = sheet.getLastRow() // 情報がなければスルー if (lastRow == 1) return[] const itemList = sheet.getRange(2, 1, lastRow - 1, 2).getValues() return itemList } insertDataToTargetSheet:Google Sheetに一行情報を追記する こちらはGoogle Sheetの一番最後の行に情報を追記します。こちらも同様に「 Google Apps Script スプレッドシート編 初心者向け 」でまとめています。 function insertDataToTargetSheet(url = "test") { // SHEET_ID・SHEET_NAMEの情報を更新してください const file = SpreadsheetApp.openById("SHEET_ID") const sheet = file.getSheetByName("SHEET_NAME") const lastRow = sheet.getLastRow() sheet.getRange(lastRow + 1, 1).setValue(url) } 引数でテキスト情報(URL)を受け取り、その情報をSheetに記入しています。 実行可能APIとしてデプロイする 実行可能APIとしてデプロイする前にプロジェクトをGCPと接続する必要があります。これはGASをAPI経由で実行する際、GCPプロジェクトから認可を発行して権限を確認するためです。 あとは、デプロイを作成しましょう。 GASが使用しているOAuthスコープを確認する プロジェクトの概要に移動するとGASプロジェクトが使用しているOAuthスコープを確認することができます。こちらは、実行可能APIとしてデプロイ後、外部からAPIをたたく際に取得するアクセストークンのスコープに収める必要があります。 今回であれば、以下のスコープになります。 スコープ 概要説明 https://www.googleapis.com/auth/script.external_request GASから外部のAPIへアクセスする際に必要(UrlFetchApp) https://www.googleapis.com/auth/spreadsheets GASからGoogle Sheetを操作するのに必要 Google Script Run実行 構築に必要なエンドポイントは4つになります。フロントエンド側から実行する順番に解説をしていきます。 有効なトークンを保持しているか検証エンドポイント /api/google-auth/verify 200の場合はトークン発行済み 401の場合は認可フロー開始:URL発行 Google認可フロー OAuth2.0認可用URL発行エンドポイント /api/google-auth OAuth2.0 Callbackエンドポイント /api/google-auth/callback Google Script Run実行用エンドポイント /api/google-auth/test ソースコードは長くなるので、最後にまとめて記載します。Gitのリポジトリとしては、 こちら を参照してください。 Google認可プロバイダー設定 Googleの認可プロバイダーの設定をする必要があります。「承認済みのJavaScript生成元」「承認済みのリダイレクトURI」は適宜設定してください。 ローカルで検証する場合は、以下の値を設定していました。赤枠の値は後で必要になるので、値をコピーしておいてください。 プロパティ 値 承認済みのJavaScript生成元 http://localhost:5000 承認済みのリダイレクトURI http://localhost:5000/api/google-auth/callback 次に API Library にアクセスして必要になるAPIを有効化させます。 Apps Script API を有効にしています。 nest.jsで開発するためには Google Auth Library を導入する必要があります。 npm install google-auth-library クライアントの作成には赤枠から情報を取得した情報を使用する必要があります。 import { OAuth2Client } from 'google-auth-library'; const client = new OAuth2Client({ clientId: "CLIENT ID", clientSecret: "CLIENT SECRET", redirectUri: "REDIRECT URI", }); 環境変数としてはConfigurationを使用して 保存しておけばアクセスがしやすくなります。 1. トークンを取得済みか検証する こちらのエンドポイントでは、Cookiesにトークンが保持されているかを確認します。Cookieに保存されているトークンを検証して、期限切れの場合は認可用のURLを発行して認可フローへ誘導します。 実装パターンとしては、401のエラーメッセージを拡張して認可用URLを埋め込んで返答しています。クライアント側で一度 /api/google-auth/veify を叩くことで認可まで一気に進めることができます。 2. Google認可フロー 認可フロー開始からアクセストークン取得までを一気に解説します。認可用URLにリダイレクトするとGoogleの画面が入るのでアカウント情報を入力すると、リダイレクトURIに設定したパスに認可コード付き(クエリ)でコールバックが返ってきます。 認可コードからIDトークンとアクセストークンを取得することができ、Cookiesに情報を保持します。Cookiesの保存期間としては1時間を保存期間としています。 最終的に好きな画面にリダイレクトさせれば完了です。 3. Google Script Run実行フロー 実行可能APIを外部から実行するためには、実行可能APIのスクリプトID・アクセストークン・実行したい関数名が必要になります。 公式リファレンスとしてはこちらになります 。 ヘッダーにアクセストークンを挿入して、URLはスクリプトIDを挿入したURLになります。 const response = await fetch(`https://script.googleapis.com/v1/scripts/${scriptId}:run`, { method: 'POST', headers: { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ function: functionName, parameters: parameters || [], }), }); 送信するBodyの中身としては、 function で実行したい関数名を指定して、引数は parameters の配列に収めて送信することで渡すことができます。型定義としては以下になります。 { "function": string, "parameters": [ value ], } 検証用フロントエンド画面 クライアントで検証するために簡易的な画面を作成します。ソースコードの原文としては、 こちら に上がっています。 useGoogleOAuth:認可処理用カスタムHook "use client"; import { useEffect, useState } from "react"; export const useGoogleOAuth = () => { const [isLoading, setIsLoading] = useState(true); useEffect(() => { const verify = async () => { const res = await fetch("/api/google-auth/verify"); if (res.status === 200) { setIsLoading(false); } else if (res.status === 401) { const data = await res.json(); console.log(data); window.location.href = data.url; } else { const data = await res.json(); alert(`認証に失敗しました。:${data.message}`); } }; if (isLoading && typeof window !== "undefined") { verify(); } }, [isLoading]); return { isLoading }; }; 処理は単純です。 /api/google-auth/verify にアクセスして、401が出たら認可用URIに遷移します。認可が完了するまでは isLoading で状態を管理します。 検証ページ "use client"; import { useActionState } from "react"; import { LoadingMainComponent } from "@/components/LoadingMainComponent"; import { useGoogleOAuth } from "@/hooks/useGoogleOAuth"; export default function GooglePage() { const { isLoading } = useGoogleOAuth(); if (isLoading) return <LoadingMainComponent />; const onClickRead = async ( action: "healthCheckFunction" | "getSheetAllData" ) => { const res = await fetch("/api/google-auth/test", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ functionName: action }), }); if (res.status === 200) { const data = await res.json(); console.log(data); } else { const data = await res.json(); alert(`Error: ${data.message}`); } }; return ( <> <main className="flex w-full flex-col gap-2"> <div className="flex max-w-xl flex-col gap-2 p-4"> <Button label={"Hello World!"} onClick={() => onClickRead("healthCheckFunction")} /> <Button label={"Get Sheet All Data"} onClick={() => onClickRead("getSheetAllData")} /> <FormComponent /> </div> </main> </> ); } type ButtonProps = { label: string; } & React.ButtonHTMLAttributes<HTMLButtonElement>; const Button = (props: ButtonProps) => { const { label, onClick } = props; return ( <button onClick={onClick} className="flex items-center justify-center rounded-lg bg-white px-8 py-2 shadow transition-all hover:-translate-x-1 hover:-translate-y-1 hover:cursor-pointer hover:shadow-md" > {label} </button> ); }; type FormType = { url: string; }; type PrevFormDataType = { value: FormType; validationError: { url: Error | null }; apiError: Error | null; }; const FormComponent = () => { const initialFormData: PrevFormDataType = { value: { url: "" }, validationError: { url: null }, apiError: null, }; const validationUrl = (url: string) => { try { new URL(url); return null; // URLが有効な場合はエラーなし } catch (e) { return new Error(`Invalid URL format ${e}`); // 無効なURLの場合はエラーを返す } }; const [formData, action, isPending] = useActionState< PrevFormDataType, FormData >(async (_: PrevFormDataType, formData: FormData) => { // FormDataをobjectに変換 const _formData = Object.fromEntries(formData.entries()); const data: FormType = { url: _formData.url as string, }; // validationを掛ける いい感じのライブラリがあれば参考にする const urlError = validationUrl(data.url); if (urlError) { return { value: { url: data.url }, validationError: { url: urlError, }, apiError: null, }; } // ここでAPI処理を実装・今回は2秒待ってエラーを返す const res = await fetch("/api/google-auth/test", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ functionName: "insertDataToTargetSheet", params: [data.url], }), }); if (res.status === 200) { alert("Data submitted successfully!"); return { value: { url: "" }, validationError: { url: null }, apiError: null, }; } const apiError = new Error("Failed to submit data"); return { value: { url: data.url }, validationError: { url: urlError, }, apiError: apiError, }; }, initialFormData); return ( <> <form action={action} className="flex w-full max-w-xl flex-col gap-2 rounded-md p-4 shadow" > <label className="flex flex-col"> <div className="flex flex-row text-xl"> <span className="w-1/3">名前:</span> <input className="w-full border p-1 text-right" type="text" name="url" defaultValue={formData.value.url} /> </div> <span className="h-4 text-xs text-red-500"> {formData.validationError.url && ( <>{formData.validationError.url.message}</> )} </span> </label> <button className={ "w-full rounded-md py-4 text-lg text-white" + (isPending ? " bg-gray-400" : " bg-blue-500") } type="submit" formAction={action} disabled={isPending} > 送信{isPending && "中"} </button> <span className="h-4 text-xs text-red-500"> {formData.apiError && <p>{formData.apiError.message}</p>} </span> </form> </> ); }; 実行可能APIの検証のために3つのパターンで /api/google-auth/test にリクエストを送信しています。 ソースコード 環境変数吸出し用env service import { MessagingApiClient } from '@line/bot-sdk/dist/messaging-api/api'; import { Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { OAuth2Client } from 'google-auth-library'; @Injectable() export class EnvironmentsService { constructor(private configService: ConfigService) {} get GoogleClientID(): string { return this.configService.get('GOOGLE_CLIENT_ID'); } get GoogleClientSecret(): string { return this.configService.get('GOOGLE_CLIENT_SECRET'); } get GoogleRedirectUri(): string { return this.configService.get('GOOGLE_CALLBACK_URL'); } GoogleOAuth2Client() { const client = new OAuth2Client({ clientId: this.GoogleClientID, clientSecret: this.GoogleClientSecret, redirectUri: this.GoogleRedirectUri, }); return client; } get GoogleScriptURL(): string { return this.configService.get('GAS_SCRIPT_URL'); } get isProduction(): boolean { const env: string = this.configService.get('ENV'); if (env === 'development') { return false; } else { return true; } } } Guards import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common'; import { EnvironmentsService } from 'src/config/enviroments.service'; import { Request } from 'express'; @Injectable() export class IsGoogleIdTokenVerifyGuard implements CanActivate { constructor(private readonly env: EnvironmentsService) {} async canActivate(context: ExecutionContext): Promise<boolean> { const request = context.switchToHttp().getRequest<Request>(); const idToken = request.cookies['id_token']; console.log('verify idToken', ':come on'); if (!idToken) return false; const isValid = await this.verfyIdToken(idToken); if (!isValid) return false; return true; } private async verfyIdToken(idToken: string): Promise<any> { const client = this.env.GoogleOAuth2Client(); try { const ticket = await client.verifyIdToken({ idToken: idToken, audience: this.env.GoogleClientID, }); const payload = ticket.getPayload(); const now = Math.floor(Date.now() / 1000); // 現在時刻(秒単位) if (payload && payload.exp && payload.exp > now) { return true; // トークンは有効 } else { return false; // トークンは無効または期限切れ } } catch (error) { return false; // トークンが無効の場合 } } } Controller import { Body, Controller, Get, Post, Query, Req, Res, UseGuards } from '@nestjs/common'; import { IsGoogleIdTokenVerifyGuard } from 'src/common/guard/is-google-id-token-verify/is-google-id-token-verify.guard'; import { EnvironmentsService } from 'src/config/enviroments.service'; import { GoogleAuthService } from './google-auth.service'; import { RequestScriptRunDto } from './dto/request.dto'; @Controller('/api/google-auth/') export class GoogleAuthController { constructor( private readonly googleAuthService: GoogleAuthService, private readonly env: EnvironmentsService, ) {} // Google認証のURLを取得する @Get() async getGoogleAuthUrl(@Res() res): Promise<void> { const authUrl = await this.googleAuthService.getGoogleAuthUrl(); res.redirect(authUrl); } // Google認証のコールバックURL @Get('callback') async getGoogleAuthCallback(@Query('code') code: string, @Res() res): Promise<void> { const tokens = await this.googleAuthService.getToken(code); res.cookie('id_token', tokens.id_token, { httpOnly: this.env.isProduction, secure: this.env.isProduction, sameSite: 'Strict', maxAge: 3600 * 1000, // 1時間 }); // 環境によってbooleanを切り替える res.cookie('access_token', tokens.access_token, { httpOnly: this.env.isProduction, secure: this.env.isProduction, sameSite: 'Strict', maxAge: 3600 * 1000, // 1時間 }); res.redirect('/community/google/'); } // Google認証のトークンを検証する @Get('verify') async verifyIdToken(@Req() req, @Res() res): Promise<void> { const idToken = req.cookies['id_token']; if (!idToken) { const authUrl = await this.googleAuthService.getGoogleAuthUrl(); res.status(401).json({ message: 'No id_token', url: authUrl }); } // token validation const isValid = await this.googleAuthService.verfyIdToken(idToken); if (isValid) { res.status(200).json({ message: 'Valid access token' }); } else { const authUrl = await this.googleAuthService.getGoogleAuthUrl(); res.status(401).json({ message: 'No id_token', url: authUrl }); } } @Post('test') @UseGuards(IsGoogleIdTokenVerifyGuard) async test( @Req() req, @Body() body: RequestScriptRunDto, @Res() res, ): Promise<string | undefined | { url: string; content: string }[]> { const accessToken = req.cookies['access_token']; console.log('accessToken', req); const result = await this.googleAuthService.runScript(accessToken, body.functionName, body.params); return res.status(200).json(result); } } Service import { Injectable } from '@nestjs/common'; import { Credentials } from 'google-auth-library'; import { EnvironmentsService } from 'src/config/enviroments.service'; @Injectable() export class GoogleAuthService { constructor(private readonly env: EnvironmentsService) {} async getGoogleAuthUrl(): Promise<string> { const client = this.env.GoogleOAuth2Client(); const authUrl = client.generateAuthUrl({ scope: [ '<https://www.googleapis.com/auth/userinfo.profile>', '<https://www.googleapis.com/auth/script.scriptapp>', '<https://www.googleapis.com/auth/script.external_request>', '<https://www.googleapis.com/auth/spreadsheets>', ], redirect_uri: this.env.GoogleRedirectUri, }); return authUrl; } async verfyIdToken(idToken: string): Promise<any> { const client = this.env.GoogleOAuth2Client(); try { const ticket = await client.verifyIdToken({ idToken: idToken, audience: this.env.GoogleClientID, }); const payload = ticket.getPayload(); const now = Math.floor(Date.now() / 1000); // 現在時刻(秒単位) if (payload && payload.exp && payload.exp > now) { return true; // トークンは有効 } else { return false; // トークンは無効または期限切れ } } catch (error) { console.error('Error verifying access token:', error); return false; // トークンが無効の場合 } } async getToken(code: string): Promise<Credentials> { const client = this.env.GoogleOAuth2Client(); const tmp = await client.getToken(code); console.log(tmp); const { tokens } = tmp; return tokens; } // <https://developers.google.com/apps-script/api/reference/rest/v1/scripts/run?hl=ja> async runScript( accessToken: string, functionName: 'healthCheckFunction' | 'getSheetAllData' | 'insertDataToTargetSheet', parameters: (string | number)[] | undefined, ): Promise<string | undefined | { url: string; content: string }[]> { const url = this.env.GoogleScriptURL; const response = await fetch(url, { method: 'POST', headers: { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ function: functionName, parameters: parameters || [], }), }); const data = await response.json(); if (response.status !== 200 || data.error) { console.error('Error calling Google Apps Script:', data.error); throw new Error(`Error: ${data.error.message}`); } const result = data.response.result; if (typeof result === 'undefined') return; if (typeof result === 'string') return result; if (Array.isArray(result)) { const temp = result.map((item: { url: string; content: string }) => { return { url: item.url || '', content: item.content || '', }; }); return temp; } return result; } } おわり GASを実行可能APIとして公開し、OAuth2.0による認証を実装することで、セキュアなAPIエンドポイントを作成することができました。今回実装したコードは、Google Sheetsとの連携も含めて、実際のプロダクションで使用可能なレベルのものとなっています。 今後は、エラーハンドリングやログ機能の追加など、より堅牢な実装に向けて改善を進めていきたいと思います。 ご覧いただきありがとうございます! この投稿はお役に立ちましたか? 役に立った 役に立たなかった 0人がこの投稿は役に立ったと言っています。 The post GAS × OAuth2.0:実践で使える実行可能API構築の手順 first appeared on SIOS Tech. Lab .
OSSよろずサポート担当の神﨑です。 お問い合わせとしてよく来るエラーメッセージについて解説していこうと思います。 今回は、AH02608、AH10154、AH01095 AH02608: read request body failed このエラーは何のエラー? クライアントからのリクエストボディの読み取りに失敗した旨のメッセージです。リクエストボディの送信が途中で中断してしまった場合や、ネットワークに問題があった場合などに出力されます。 AH2609 と何が違うの? 同じエラーメッセージが出力されるエラーとして AH2609 があります。 AH2609 は Content-Length ヘッダー (事前にリクエストボディのサイズが指定される)で送信される際に、事前に指定されたサイズと違う場合や通信の切断が発生した場合に出力されます。 AH2608 はリクエストボディの形式がチャンクエンコーディング (事前にリクエストボディのサイズの指定がない)の場合のエラーです。 AH10154: pass request body failed このエラーは何のエラー? バックエンドへのリクエストボディ送信が失敗した旨のメッセージです。AH02608 の事象によりリクエストボディが正しく読み取れず、バックエンドへ送信できなかった可能性があります。 AH01095: prefetch request body failed このエラーは何のエラー? クライアントからのリクエストボディの事前取得に失敗してしまった旨のメッセージです。AH02608 の事象によりリクエストボディが正しく読み取れず、事前取得できなかった可能性があります。 3つのエラーはどう違うのか? どれもリクエストボディの処理が失敗しているメッセージとなりますが、読み取り、事前取得、送信と出力される処理のフェーズに違いがあります。 どう対策すればいいの? ネットワークの設定や、Timeout ディレクティブを見直したり、リクエストをクライアント側が強制終了しなかったかなどを確認する。 Timeout ディレクティブは以下の値を目安に変更する。 クライアント側のリクエスト全体を受信する時間>Timeout ディレクティブ 参考 AH02608、AH10154 については以下もご確認ください httpd mod_proxy logs ‘Partial results are valid but processing is incomplete’ error – Red Hat Customer Portal ※Red Hat社の有料ポータルログインIDが必要です。 AH02609 については以下もご確認ください。 Getting error “AH02609: read request body failed” in Apache HTTPD mod_proxy – Red Hat Customer Portal ※Red Hat社の有料ポータルログインIDが必要です。 AH1095 については以下もご確認ください。 Intermitent timeout errors in Apache HTTPD during request body prefetch ※Red Hat社の有料ポータルログインIDが必要です。 ご覧いただきありがとうございます! この投稿はお役に立ちましたか? 役に立った 役に立たなかった 0人がこの投稿は役に立ったと言っています。 The post [Apache エラー解説]プロキシを使用する際によく出力されるエラーの原因と対策 first appeared on SIOS Tech. Lab .
はじめに こんにちは、サイオステクノロジーの小沼 俊治です。 以前公開した以下の記事では、ローカル環境で動作するオリジナルの MCP サーバーを開発する手順を案内しました。今回はその続編として、ローカル MCP サーバーを Remote MCP サーバーへ改良し、リモートサーバーで動作させる方法を共有します。 オリジナルのちょっと便利な MCP サーバー を作ってみた | SIOS Tech. Lab 構成概要 筆者が動かした際の主な構成要素は以下の通りです。 Windows 10 Professional WSL 2.4.12.0 Ubuntu 24.04.2 LTS Node.js v22.15.0 Claude desktop for Windows version 0.9.3 Visual Studio Code version 1.99.3 ハンズオンを構成する環境は以下の通りです。 MCP ホストは、Claude desktop for Windows のネイティブアプリ版を利用します。 2025年5月現在、Web 版 Calude で Remote MCP サーバーを利用できるのは、Max、Team、Enterprise プランのみです。Pro、および Free プランにはまだロールアウトされてないため、desktop 版を利用します。 リモートMCPを使用したカスタム統合について | Anthropicヘルプセンター Claude から Remote MCP サーバーへは、remote-mcp の npm パッケージを介して連携するため、Windows 環境に Node.js を必要とします。 Remote MCP サーバーの構成における主な考慮事項は以下の通りです。 WSL 環境に構築する Ubuntu を、Remote MCP サーバーが動作するリモートサーバーとして扱います。 Node.js の Express パッケージを利用して HTTP プロトコルで 8787 ポートをリッスンします。 トランスポートには、ローカル MCP サーバーの STDIO から、Remote MCP サーバーでは Server-Sent Events(SSE)を使います。 Remote MCP サーバーで提供するツールの機能は、改良前の MCP サーバーと同じです。 オリジナルのちょっと便利な MCP サーバー を作ってみた | SIOS Tech. Lab ハンズオンの手順で作成する Remote MCP サーバーの設定ファイルやソースコードは、以下の GitHub リポジトリで公開しています。手順と合わせてご確認ください。 hands-on-mcp-sios-apisl @ GitHub v2.0.0 :改良後の Remote MCP サーバー(SSE)の完成状態 v1.0.0 :完了前のローカル MCP サーバー(STDIO)の完成状態 基礎環境の構築 Remoe MCP サーバー環境 on WSL WSL 環境の Ubuntu 側に Remote MCP サーバーの稼働環境を構築します。 WSL 環境の構築 以下手順を参考に Windows PC へ Linux ディストリビューション(Ubuntu)環境を用意します。 初期環境構築: WSL 環境 on Windows 10 Node.js インストール JavaScript で実装された Remote MCP サーバーを実行するために Ubuntu へ Node.js をインストールします。 $ sudo apt install -y npm $ node -v v18.19.1 apt でインストールされる Node.js はバージョンが古いためアップデートします。 $ sudo npm install n -g $ sudo n 22.15.0 インストールしたコンソールを一度閉じて新たにコンソールを開き直し、導入したバージョンをセッションに反映します。 $ node -v v22.15.0 MCP ホスト環境 on Windows Windows 側に MCP ホストの稼働環境を構築します。 Claude desktop for Windows インストール MCP ホストに Web 版ではなくアプリ版の Claude desktop for Windows を利用するため、以下公式手順を参考に Windows PC へインストールします。 デスクトップ版Claudeのインストール | Anthropicヘルプセンター Node.js インストール MCP ホストの Claude から Remote MCP サーバーのツールへアクセスする際に、Node.js の「remote-mcp」npm パッケージを介して実行するため、以下手順を参考に Windows PC へ Node.js をインストールします。 初期環境構築: Node.js on Windows Visual Studio Code インストール WSL 環境の Ubuntu で TypeScript ソースコードを実装するためのエディターとして、Windows 環境にインストールした Visual Studio Code (VS Code) を WSL 環境へリモート接続して利用する方法をお薦めします。VS Code のインストール手順については、ネット上でたくさんのコンテンツが親切に説明されているので、本資料の範囲外とさせて頂きます。 Visual Studio Code インストール – Google 検索 なお、VS Code 以外のお気に入りのエディターを利用される場合は、文中の VS Code をご利用のエディターに置き換えて読み進めてください。 Remote MPC サーバーへ改良 WSL 環境の Ubuntu で Remote MCP サーバーを開発するため、Ubuntu にログインし、ハンズオン用のフォルダを作成して作業を進めます。 $ mkdir -p ~/handson/ $ cd ~/handson/ 改良前のローカル MCP サーバーを用意 以前掲載した以下の記事を参考に、Remote MCP サーバーの改良元となるローカル MCP サーバーを Ubuntu 環境に用意してください。なお、当該記事は Windows 環境で動作するローカル MCP サーバーの構築について記載していますが、今回の Remote MCP サーバーは Ubuntu 環境で動作させるため、Ubuntu 環境に用意してください。 オリジナルのちょっと便利な MCP サーバー を作ってみた | SIOS Tech. Lab また、改良前のローカル MCP サーバーのソースコードは、以下の GitHub リポジトリに「v1.0.0」タグで保存しているので、Git コマンドで取得して用意頂くこともできます。 hands-on-mcp-sios-apisl v1.0.0 @ GitHub $ git clone https://github.com/Toshiharu-Konuma-sti/hands-on-mcp-sios-apisl.git -b v1.0.0 プロジェクト設定の改良 改良作業前に、現在設定されている「 package.json 」に従い依存パッケージをインストールします。 $ npm install Remote MCP サーバーの改良に当たり、追加で必要になる依存パッケージをインストールします。 $ npm install express @types/express ソースコードの改良 Remote MCP サーバーで起動するように「 src/index.ts 」の3か所を改良します。 まずは1点目の改良として、主に利用するトランスポートに関連する依存パッケージを置き換えます。以下の差分イメージでは、行頭の「+」は追加する行、「-」は削除する行を表します。 import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; - import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; + import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js"; + import express from "express"; import { z } from "zod"; : 次に2点目の改良として、STDIO トランスポートの実装を削除します。 - async function main() { - const transport = new StdioServerTransport(); - await server.connect(transport); - console.error("MCP Server running on stdio"); - } - - main().catch((error) => { - console.error("Fatal error in main():", error); - process.exit(1); - }); 最後の3点目の改良として、STDIO トランスポートが実装されていた箇所に、HTTP で通信する Server-Sent Events (SSE) トランスポートを実装します。 + const app = express(); + const port = 8787; + let transport: SSEServerTransport | null = null; + + app.get("/sse", async (_, res) => { + console.log("Received connection"); + transport = new SSEServerTransport("/message", res); + await server.connect(transport); + }); + + app.post("/message", async (req, res) => { + console.log("Received message"); + transport?.handlePostMessage(req, res); + }); + + app.listen(port, () => { + console.log("Remote MCP server listening on port", port); + }); 改良後のできあがりは以下を参考にしてください。 src/index.ts ビルドしてトランスパイル 改良後のできあがりを動作確認するために、ビルドして TypeScript から JavaScript のソースコードを生成します。 $ npm run build > hands-on-mcp-sios-apisl@1.0.0 build > tsc MCP Inspector で動作確認 MCP Inspector と Remote MCP サーバーを起動するには、2つのコンソールを使用します。まず、1つ目のコンソールで MCP Inspector を起動します。 $ npx -y @modelcontextprotocol/inspector@latest Starting MCP inspector... Proxy server listening on port 6277 MCP Inspector is up and running at http://127.0.0.1:6274 コンソールログに表示された URL のホスト部「127.0.0.1」を「localhost」に置き換え、その URL(例: http://localhost:6274 )でブラウザから MCP Inspector にアクセスします。 次に2つ目のコンソールを立ち上げて Remote MCP サーバーを起動します。 $ node build/index.js Remote MCP server listening on port 8787 Received connection MCP Inspector をアクティブにして、Transport Type 項目に「SSE」を選択し、URL 項目は Remote MCP サーバーを起動したホスト名とコンソールログに表示されたポート番号に「/sse」のパスを使った URL(例: http://localhost:8787/sse )を入力します。「Connect」ボタンをクリックすると MCP Inspector が Remote MCP サーバーに接続します。 接続したら画面中央の「List Tools」ボタンをクリックするとツール一覧が表示されます。 それ以降の動作確認は、ローカル MCP サーバーと同じため、以前の記事を参考に確認を進めます オリジナルのちょっと便利な MCP サーバー を作ってみた | SIOS Tech. Lab > MCP Inspector で動作確認 Claude(MCP ホスト)に組み込み 連携する MCP サーバーの設定 オリジナルの Remote MCP サーバーを Claude から利用できるように設定ファイルを編集するため、Windows のスタートメニュー「A」セクションから「Anthropic > Claude」を選択して起動します。 Claude が起動したら画面左上のハンバーガーメニューから「ファイル > 設定…」を選択します。 オリジナルの Remote MCP サーバーが利用できるようにするために、「開発者」タブの「構成を編集」ボタンをクリックします。 エクスプローラーが立ち上がり、Claude の設定ファイルに該当する「 claude_desktop_config.json 」ファイルをダブルクリックしてエディターで開きます。 「 claude_desktop_config.json 」ファイルにオリジナルの Remote MCP サーバーを利用するための設定を書いて保存します。 { "mcpServers": { "mcp-remote-sios-apisl-demo": { "command": "npx", "args":[ "mcp-remote", "http://localhost:8787/sse" ] } } } 「 claude_desktop_config.json 」ファイルの編集における主な考慮事項は以下の通りです。 「mcpServers > mcp-sios-apisl-demo > args」フィールドで主な考慮事項は以下の通りです。 npx コマンドの第1引数に mcp-remote パッケージを指定することで、Retemo MCP サーバーと連携します。 npx コマンドの第2引数に mcp-remote パッケージが連携する Remote MCP サーバーの URL を指定します。 Claude の再起動で設定反映 変更した設定ファイルの適用に、ウィンドウの「×」ボタンで終了させるだけではなく、Claude のプロセスを完全に終了させる必要があるため、Windows タスクバーの画面右側のアイコン表示領域から Claude アイコン(トゲトゲのウニの様なデザイン)を探し、右クリックで表示されるメニューから「終了」を選択してプロセスを完全に終了させます。 Windows のスタートメニュー「A」セクションから「Anthropic > Claude」を選択して起動します。 Claude が起動したら画面中央のテキストボックス内の左下にある「検索とツール」アイコンをクリックします。表示されたメニューに、「mcp-remote-sios-apisl-demo」の項目と右横に数字が表示されていたら、オリジナルの MCP サーバーは正常に設定できました。 オリジナルの Remote MCP サーバーのデモ 提供するツールの機能は Remote MCP サーバーでもローカル MCP サーバーと同じため、以前の記事を参照ください。 (動画内で MCP サーバーを有効化する際、表示されているサーバー名『mcp-sios-apisl-demo』を『mcp-remote-sios-apisl-demo』に読み替えてご覧ください) オリジナルのちょっと便利な MCP サーバー を作ってみた | SIOS Tech. Lab > オリジナルの MCP サーバーのデモ まとめ リモート MCP サーバーの実装手順は、いかがでしたでしょうか?MCP はまだ発展中の仕様のため引き続き動向をウォッチしながら得られたノウハウを共有していきたいと思います。 ご覧いただきありがとうございます! この投稿はお役に立ちましたか? 役に立った 役に立たなかった 0人がこの投稿は役に立ったと言っています。 The post オリジナルのちょっと便利な『リモート MCP サーバー』を作ってみた first appeared on SIOS Tech. Lab .
ども!AI関連のブログを頑張って執筆中の龍ちゃんです。最近は、設計から開発まで幅広い範囲でのAI活用をしています。一向に仕事がなくなる気配がないですね。AIが仕事を奪うのはいつになるのでしょうか。それまでは、活用して業務効率化していかないといけないですね。 さて!今回は「GitHub CopilotのPull Request(PR)レビューにシステムプロンプトを与える」という内容になっています。GitHub Copilotをレビュワーとして活用している方の中に以下のような課題感を抱えた方がいれば、興味のある内容だと思います。 英文でのレビューを日本語化したい CopilotのPRの観点があいまい コードレビューの品質を一定化したい GitHub Copilotのレビューを向上させる GitHub Copilot on VSCodeでシステムプロンプトを追加する 「 GitHub Copilotにシステムプロンプトを挿入する方法 」についてはこちらで触れています。こちらでは、 .github/copilot-instructions.md というファイルを作成して自然言語でシステムプロンプトを設定しています。残念ながら、上記の方法ではPR発行時に読み込んでくれません。 GitHub Copilotにシステムプロンプトを追加する GitHub Copilot PRレビューにシステムプロンプトを組み込む方法としては、PRに直接記述する必要があります。 # 実施事項 <!-- ここに人間向きのPRを書く --> <!-- for GitHub Copilot review rule --> 日本語で記載してください。(Copilot向けの指示) <!-- for GitHub Copilot review rule--> PRの観点が一定の場合は、テンプレートとしてリポジトリ単位で保存しておくと効果的です。PRのテンプレートを作成する方法としては複数あります。 リポジトリのルート直下に pull_request_template.md を配置 docs ファイル直下に pull_request_template.md を配置 .github ファイル直下に pull_request_template.md を配置 docs と .github ファイルの場合は、 PULL_REQUEST_TEMPLATE というファイルを生成することで複数のテンプレートを作成することができます。 copilot-instructions と pull_request_template の違い これらの2つのファイルは、それぞれ異なる役割と特徴を持っていますが、上手く組み合わせることで効果的なPRレビュー環境を構築することができます。以下の表で違いについてまとめます。 項目 .github/copilot-instructions.md pull_request_template.md 用途 Copilot(AI)に対するシステムプロンプト・カスタム指示を設定し、PR作成やCopilot Chatなどで自動的に反映させる PR作成時の説明・チェックリスト・レビュールールなどを人間・AIの両方に提示するテンプレート 主な対象 Copilot(AI) PR作成者・レビュアー・Copilot(AI) 記述場所 .github/copilot-instructions.md .github/pull_request_template.md (または他の指定ディレクトリ) 反映タイミング Copilot ChatやAIレビュー、要約生成などCopilotの応答時 PR作成時にPR本文へ自動挿入される 人間への見やすさ 人間は通常直接見ない PR本文に表示されるため人間も確認可能 運用の柔軟性 リポジトリ全体に一括でAI指示を適用できる 複数テンプレートや内容のカスタマイズが容易 主なメリット AI応答の一貫性・自動化 PR作成の標準化・レビュープロセスの明確化・人間とAI両方に伝達可能 主なデメリット 人間には直接見えない・AIが必ずしも全て反映するとは限らない 指示がPR本文に残るためノイズになる場合も・AIへの伝達は工夫が必要 GitHub Copilot PR用プロンプト備忘録 こちらはまだ検証中の内容になります。運用を進めてみて、進展があればブログにてまとめて行きます。 コメントは絶対日本語でほしい これは、マストで入れておきたい内容です。別に翻訳アプリを使うので、読めなくはないのです。ただ圧倒的に気分が駄々下がりになるので、絶対日本語化はしておきたいです。 絶対日本語で出力してください。 無視しておきたいことを「禁止事項」として追記 これは、開発時における不満です。Typescript環境で console.log を一生注意されるんです。検証時には残しておきたいですが、気分的にコンソールでトークン数が消費されるの気持ち的にげんなりです。 そのため、不要な指摘を減らすために以下のようなプロンプトを追加しています。 以下の点については、レビューの対象外としてください: - console.logの使用 - 開発環境用の一時的なコメントアウト - デバッグ用の一時的な変数 このように明示的に除外項目を指定することで、より効率的なレビューが可能になります。 もちろん、最終的なコードには含まれていては困るので改めてレビュー観点への追加対応を行う必要はあります。 おわり 今回は、GitHub CopilotのPRレビューをより効果的に活用するための方法として、システムプロンプトの追加方法とPRテンプレートの活用について紹介しました。AIと人間の両方に役立つ指示を設定することで、レビューの品質向上と一貫性を実現できます。これらのツールを上手く組み合わせることで、より効率的な開発プロセスを構築していきましょう。 ご覧いただきありがとうございます! この投稿はお役に立ちましたか? 役に立った 役に立たなかった 0人がこの投稿は役に立ったと言っています。 The post PRレビューを自動化しよう!GitHub Copilot × システムプロンプトの基本 first appeared on SIOS Tech. Lab .
お疲れ様です。最近はブログから離れて隠遁生活をしていました。社内で「GitHub Copilot」の普及のためにイベントなどをやっていました。基本的な部分から実践的な使い方までをまとめていました。今回は「GitHub Copilot」でシステムプロンプトを設定して、チーム内で一定のGitHub Copilot出力を担保する方法について紹介していきます。 今回紹介する方法はパブリックプレビュー状態なので、変更される可能性があります。 最新情報 を追って合わせて更新していきましょう。 前提条件 開発環境としてはVisual Studio Code(VSCode)でGitHub Copilot businessでの開発を前提としています。 システムプロンプト設定方法 システムプロンプトを設定する方法としては、2ステップで設定することができます。 VSCodeの設定で「 Code Generation: Use Instruction Files 」の設定を有効化する .github/copilot-instructions.md を作成してシステムプロンプトを設定する VSCodeの設定有効化 VSCodeで「Shift+,」を開いて設定を開きましょう。検索欄にgithub.copilot.chat.codeGeneration.useInstructionFilesと入力するか、 こちら をクリックして設定項目に遷移してください。 項目を true にすると設定自体は完了です。 .github/copilot-instructions.md を作成してシステムプロンプトを設定する ルートディレクトリに .github ディレクトリを作成し、 copilot-instructions.md というファイルを作成します。作成したファイルに自然言語でプロンプトを記述することで機能が有効化されます。以下のようなディレクトリ構成になっていれば設定自体は完了です。 わかりやすいようにエンタメよりのシステムプロンプトを設定してみました。index.htmlの解説をしてもらうと以下のような出力が返答されており、適切に設定が完了しています。 GitHub Copilotの出力サンプル 入力 class="bg-gradient-to-r from-indigo-600 to-purple-600 text-white py-6 上記のソースを解説して 出力 以下のコードは、HTMLの <header> 要素に適用されているクラス属性の一部です。このクラス属性は、Tailwind CSSを使用してスタイルを設定しています。 class="bg-gradient-to-r from-indigo-600 to-purple-600 text-white py-6 bg-gradient-to-r : 背景にグラデーションを適用します。 to-r は「右方向へのグラデーション」を意味します。 from-indigo-600 : グラデーションの開始色を指定します。 indigo-600 はTailwind CSSで定義されたインディゴ色の濃いトーンです。 to-purple-600 : グラデーションの終了色を指定します。 purple-600 はTailwind CSSで定義された紫色の濃いトーンです。 text-white : テキストの色を白に設定します。 py-6 : 上下のパディングを設定します。 py は「padding-y(上下方向のパディング)」を意味し、 6 はTailwind CSSのスケールに基づく値(約1.5rem)です。 このクラス設定により、ヘッダー部分に右方向へのインディゴから紫へのグラデーション背景が適用され、テキストは白色で、上下に適度な余白が確保されていますにゃー。 こちらのファイルはそのままリポジトリに上げることで、新たに参入する開発メンバーも自然に導入することができます。 システムプロンプトの備忘録 こちらはまだ検証中の内容になります。運用を進めてみて、進展等があればブログにてまとめて行きます。 不必要なプロンプトは除外しておく 今回はデモ目的だったので、語尾に「にゃー」とつけてもらいました。実用性に関しては皆無であり、トークン数の無駄な消費につながります。GitHub Copilotはコード関連に特化したAIとなっています。そのため、汎用的な質問などは受け付けません。 システムプロンプトには、コードに関する情報を入れておくことが好ましいですね。 フロントエンドエンジニアならこんなプロンプトもよいかも 前提情報として、リポジトリ内で使用している技術の概要あたりは書いておくのが良いかもしれません。GitHub Copilotは特に意識をしなければ、開いているファイル群を読み取り回答を生成します。Tailwindでカスタム定義したCSSなどは、あまり読み込んでくれない印象です。そのため、以下のような情報を箇条書きで与えています。 Next.js+Typescript+Tailwind CSS構成 Tailwind CSSベースでのスタイル情報 コンセプト(モダン・シック・かっこいい など…) おわり GitHub Copilotでシステムプロンプトを設定することで、チーム内での一貫性のある開発体験を実現することができます。今回紹介した方法は、まだパブリックプレビュー段階ですが、今後の発展に期待が持てる機能です。ぜひ、チームの開発スタイルに合わせてカスタマイズしてみてください。 ご覧いただきありがとうございます! この投稿はお役に立ちましたか? 役に立った 役に立たなかった 0人がこの投稿は役に立ったと言っています。 The post GitHub Copilotをチーム開発で使いこなす!システムプロンプト設定方法 first appeared on SIOS Tech. Lab .
  こんにちは、サイオステクノロジーの佐藤 陽です。 今回はEntraIDのトークン構成の設定に関する内容になります。 自分が開発を進めていく中で 「トークン構成の設定でオプション要求の追加したにもかかわらず、取得されたアクセストークンの中には設定した値が入っていない」 といったケースに遭遇しました。 こちらの問題点を解消する方法や、解決策に関わる知識部分をご紹介したいと思います。 はじめに EntraID は IdP (Identity Provider) として、認証認可の仕組みを実現するために広く利用されています。 そしてEntraIDには、得られるアクセストークンやIDトークンの構成をカスタマイズできる機能があります。 この機能を利用し、とある情報をアクセストークンに含めようとしたのですが、トークンの構成を行ったにも関わらず、必要な情報が含まれないといった課題に直面しました。 そこで今回はこの原因と、原因を理解するための周辺知識について備忘録として書いていきたいと思います。 なお、今回の構成で利用する要素としては React/Nextjs MSAL(Microsoft Authentication Library) EntraID といったものになります。 とはいえ、今日のお話はEntraID以外は特に環境に依存するものではありません。 トークン構成 EntraIDでアプリケーションを作成すると、Portal上において「トークン構成」といったブレードが見られます。 選択すると オプションの要求の追加 グループ要求の追加 を行うことが可能となり、ここからトークンに含める追加情報を構成することができます。 例えば、オプション要求の追加を選択すると どのトークンに関する設定か どの要求を追加するか を設定することができます。 今回は、試しに 対象トークン 追加する要求 アクセス auth_time を設定し、アクセストークンの取得を試みます。 アクセストークンの取得 では実際にアクセストークンを取得します。 基本的はMSALのサンプルそのままを実行していきますので、詳細な実装は割愛します。 Config情報に関しては コチラ を参考に、以下の内容で設定しました。 key value clientId {EntraID上に登録したアプリケーションのクライアントID} authority https://login.microsoftonline.com/ redirectUri ローカルでの検証のため、ひとまず http://localhost:3000 scopes User.Read また、併せてEntraID上のアプリケーションの認証の設定において SPAのアプリケーションを追加し、リダイレクトURLを http://localhost としておきます。 アクセストークン内容の確認 ではこの実装に基づくアプリケーションで取得したアクセストークンの内容をDecodeしたものを一部抜粋して掲載します。 { "aud" : "00000003-0000-0000-c000-000000000000" , "iss" : "https://sts.windows.net/***/" , "iat" : 1748319065 , "nbf" : 1748319065 , "exp" : 1748322980 , "acct" : 1 , "acr" : "1" , ...(略) "xms_ftd" : "jjqoMQ6P8XOaNHnHHc-JnGBPyVrayvj9UXOQgTf1iL0BamFwYW5lYXN0LWRzbXM" , "xms_idrel" : "5 16" , "xms_st" : { "sub" : "EYjUyDvkYaYrC-pPaxE8jbp_imGQOGYEFIlRXjRG_mA" } , "xms_tcdt" : 1412690105 } すると、この中に先ほど指定したはずの auth_time の値が含まれていないことが確認できます。 なぜオプション項目が含まれていないのか 作成したアプリケーションのClientIDを正しく指定しているはずですし、設定が反映されていないのが腑に落ちません。 と、ここでDecodeした中身の aud のパラメータに注目します。 このaudのパラメータですが定義としては こちら に記載があります。 トークンの想定されている読者を識別します。 v2.0 トークンでは、この値は常に API のクライアント ID です。 v1.0 トークンでは、これは、クライアント ID、または要求で使用されるリソース URI になります。 値は、クライアントがトークンを要求した方法によって異なります。 (※今回はv2.0を利用) 現在、取得されたアクセストークンにおける aud の値は 00000003-0000-0000-c000-000000000000 です。 そしてこのaudの値はどのAPIのクライアントIDを示しているかというと、以下に示したサイトから読み解くにMicrosoftGraphのAPIとなります。 https://learn.microsoft.com/ja-jp/graph/permissions-reference https://learn.microsoft.com/ja-jp/entra/identity-platform/access-tokens つまり、 MicrosoftGraphのAPIを利用するために必要となるアクセストークンが、MicrosoftGraphのAPIを公開するアプリケーションマニフェストの情報に基づいて返されます。 マニフェストに関しては前回の記事でご紹介したので、こちらを参照ください。 【Azure】EntraIDにおけるアプリケーションマニフェストとは? 繰り返しになりますが、audの値がMicrosoftGraphのAPIのID値になっているということは そのAPIを公開するアプリケーションのマニフェストの内容に基づいてトークンの内容が決定され、返されます。 そのため、先ほど自ら設定したアプリケーションのアクセストークン構成はまったく意味を成しません。 なぜこのような状況が起きているかというと、MSALを利用する際にパラメータとして与えた Scope の設定が影響しています。 今回Scopeの設定としては、サンプルで使われていた User.Read をそのまま使ってしまっていました。 そしてこれは、MicrosoftGraphAPIの User.Read のScopeを指しています。 そのため、「このクライアントはMicrosoftGraphのAPIを利用したいんだな!」と判断されたため MicrosoftGraphのAPIを管理するアプリのマニフェストに基づいてトークンが返されてしまったのです。 これを解決するためには、自らAPIを公開し、そのAPIを利用するためのアクセストークンを発行する必要があります。 以下に実際のステップを記載します。 再取得(意図したクレームを含める方法) API の公開設定 先ほどのアプリケーションにおいて新規にAPIの公開を行います。 アプリケーションURIの発行 Scopeの追加 Scopeに関しては以下のような内容で追加します。 値としては好きなものを入力してください。 Scope の指定 MSALでアクセスする際に、今回追加したScope( api://cb769b13-3f44-41ad-abcf-444acca396af/Option.Read )にScopeを置き換えます。 こうすることで、先ほど公開したAPIにアクセスするためのアクセストークンが取得できるようになります。 export const loginRequest = { scopes : [ "api://cb769b13-3f44-41ad-abcf-444acca396af/Option.Read" ] , } ; ここまでできれば準備OKです。 トークン内容の確認 再度アクセストークンを取得し、デコードします。 { "aud" : "api://cb769b13-3f44-41ad-abcf-444acca396af" , "iss" : "https://sts.windows.net/***/" , "iat" : 1748333202 , "nbf" : 1748333202 , "exp" : 1748338754 , "acr" : "1" , "aio" : "AVQAq/8ZAAAAn3CcIgjMLmAO1FwB+2ivXeaausYBhcFitnPXL1vI7N3fh63o7WnB0jK6mH/rdhjZxTfVuRvymlC6tzAhB1disVqX7l9OhDHPF+s/qSneArw=" , "amr" : [ "pwd" ] , "appid" : "cb769b13-3f44-41ad-abcf-444acca396af" , "appidacr" : "0" , "auth_time" : 1748333497 , "email" : "ak-sato@sios.com" , "idp" : "https://sts.windows.net/47d0c615-90e7-43ad-8653-720c7bf26547/" , "ipaddr" : "123.1.7.105" , "name" : "佐藤 陽" , "oid" : "b00dbe40-3b3d-46ec-962a-4abba930da7d" , "rh" : "1.AVMA7zxCzECqM0S84dL_kXbAyhObdstEP61Bq89ESsyjlq9TAJlTAA." , "scp" : "Option.Read" , ...(略) } するとまず、 auth_time の値が正しく取得できていることが確認できました。めでたし! また、 aud の値に関しても先ほど作成したアプリケーションURIの値になっていることが分かります。 この点から、自ら作成したアプリケーションのトークン構成に紐づいてアクセストークンが返されていることがわかります。 これまでの流れを概念図に示すと以下のようになります。 Scopeとして何を指定するか audの値はどのAPIのIDとなっているか どのアプリケーションマニフェストに基づいてトークンが返ってきてるか などを意識することが重要であると考えました。 IDTokenの場合は? なお、IDトークンに関しては特にこういった設定を行わなくてもトークンの構成の設定は反映されます。 これはIDトークンの性質を考えれば分かりますが、IDTokenはAPIのアクセスに利用されるものではないためです。 そのためAPIに関わる設定に依らず、アプリケーション自体の設定に依存するため、今回のようなAPIの設定は不要となります。 まとめ 今回は、アクセストークンに自ら指定したオプション要求が含まれていない課題の解決方法および、その周辺知識の紹介を行いました。 aud のパラメータの定義 アプリケーションマニフェストの考え方 アクセストークン・IDトークンの役割 などがしっかり理解できていればすぐ分かることでしたが、なかなかそこに気づけず時間を溶かしてしまいました。 このあたりまだまだ理解が不十分なところもあるのでしっかり抑えていきたいと思います。 ではまた! ご覧いただきありがとうございます! この投稿はお役に立ちましたか? 役に立った 役に立たなかった 0人がこの投稿は役に立ったと言っています。 The post 【Azure】EntraIDのトークン構成でオプション要求が反映されない問題の解決法 first appeared on SIOS Tech. Lab .
今号では、cron によるタスクの定期実行について、その仕組みや設定方法について説明します! cron とは cron とは、 指定した時間、曜日、日付に自動でコマンドを実行してくれる デーモンの名称です。 デフォルトでもいくつかの操作が cron ジョブ により実行されるようになっており、具体的な実行内容などは設定ファイルに記述されています。 (例:logrotate の実行、特定パッケージの自動更新など) cron デーモンが常駐し、毎分設定ファイルをチェックして「 実行時間が来たか? 」を確認しています。 cron の設定ファイル、ディレクトリの配置場所 cron は下記のファイル、ディレクトリにて設定ファイルが配置されます。 まずはファイルやディレクトリの種類、役割について見ていきましょう。 (※今回は RHEL8 の環境を前提に説明します) 1. /etc/crontab システム全体の cron ジョブを設定するためのファイル。 システム管理者 (root) のみが編集できます。 (※ 通常は、このファイルを直接編集しません) 2. /etc/cron.d システム全体の cron ジョブを設定するためのファイル。 /etc/crontab ではなく、このディレクトリ配下にファイルを配置することが一般的です。 3. /etc/cron.hourly 毎時 (1時間ごと) 実行される設定ファイル (スクリプト) を配置するディレクトリ。 4. /etc/cron.daily 毎日 (1日ごと) 実行される設定ファイル (スクリプト) を配置するディレクトリ。 5. /etc/cron.weekly 毎週 (1週間ごと) 実行される設定ファイル (スクリプト) を配置するディレクトリ。 6. /etc/cron.monthly 毎月 (1ヵ月ごと) 実行される設定ファイル (スクリプト) を配置するディレクトリ。 7. /var/spool/cron ユーザごとの個別の設定ファイル (crontab) を配置するディレクトリ。 後述する crontab コマンド でタスクを作成すると、このディレクトリ配下に設定ファイルが作成されます。 cron ジョブの設定方法 (時間指定) cron によるタスクを作成するには、下記の 2通りの方法があります。 crontab コマンドで設定する (ユーザごとの個別の設定) /etc/cron.d 配下に設定ファイルを配置する (システム全体の設定) それぞれの手順を説明します。 crontab コマンドで設定する (ユーザごとの個別の設定) 各ユーザごとに、現在どのような cron のタスクが設定されているかを確認するには crontab -l コマンドを実行します。 デフォルトでは何も登録されていないため、下記のような表示になります。 $ crontab -l no crontab for ykaino cron のタスクを追加、編集するには crontab -e コマンドを実行します。 テキストエディタが開きます。 cron は、 分、時、日、月、曜日 の 5つのフィールドで実行タイミングを指定します。 * * * * * コマンド 例1:毎日 0時に実行: 0 0 * * * 例2:毎週月曜日の朝 6時に実行: 0 6 * * 1 例3:毎分実行: * * * * * 例えば、決まった時間に特定のスクリプトを動作させたい場合、下記のように設定し、保存 ([:w]、もしくは[ZZ]) します。 30 7 * * * /path/to/myscript.sh ※スクリプトは絶対パスで指定しておくと確実です。 なお、 crontab -e でタスクを追加後 crontab -l を再度実行してみると、下記のようにタスクが追加されていることが分かります。 $ crontab -l 30 7 * * * /path/to/myscript.sh /etc/cron.d 配下に設定ファイルを配置する (システム全体の設定) システム全体に適用されるタスクを追加したい場合、crontab コマンドではなく /etc/cron.d 配下に直接ファイルを作成します。 タスクの設定方法は、crontab コマンドで実施した方法と同じです。 例えば、上の例でも出した myscript.sh をシステム全体で適用したい場合、下記のように設定します。 # cat /etc/cron.d/myscript 30 7 * * * /path/to/myscript.sh なお、設定ファイル追加後は cron を再起動しなくてもタスクが適用されます。 cron ジョブの設定方法 (スクリプト) cron のタスクは、時間指定する方法だけでなくスクリプト形式でも登録することができます。 単純なコマンド実行だけでなく、処理を分岐させたい場合や、より複雑な処理が必要な場合はスクリプト形式での登録が有用です。 例として、デフォルトで配置されている /etc/cron.daily/logrotate の内容を見てみます。 1 #!/bin/sh 2 3 /usr/sbin/logrotate /etc/logrotate.conf 4 EXITVALUE=$? 5 if [ $EXITVALUE != 0 ]; then 6 /usr/bin/logger -t logrotate "ALERT exited abnormally with [$EXITVALUE]" 7 fi 8 exit $EXITVALUE 3行目 logrotate コマンド (/usr/sbin/logrotate) が logrotate の設定ファイル (/etc/logrotate.conf) を読み込みます。 4行目 直前に実行されたコマンド (ここでは logrotate コマンド) の 終了ステータス を EXITVALUE に格納します。 5~7行目 EXITVALUE が 0 以外 (つまり logrotate がエラーで終了) の場合、ログにエラーを示す旨のメッセージを書き込む処理を実行します。 8行目 スクリプトの終了ステータスを EXITVALUE と同じ値に設定します。 次号について 次号では、 cron タスクを追加する際の tips や、 スクリプト形式のタスク についてもう少し詳しく説明します! ご覧いただきありがとうございます! この投稿はお役に立ちましたか? 役に立った 役に立たなかった 0人がこの投稿は役に立ったと言っています。 The post 知っておくとちょっと便利!cron によるタスク管理1 first appeared on SIOS Tech. Lab .
こんにちは! 今月も「OSSのサポートエンジニアが気になった!OSSの最新ニュース」をお届けします。 今後リリース予定の Linux 6.15 カーネルをもって、「i486」と初期「Pentium」プロセッサのサポートが終了することになりました。 「Linux」で「i486」と初期「Pentium」のサポートが終了へ https://japan.zdnet.com/article/35232760/ マイクロソフトは、Microsoft Azure 上で動作する新たなディストリビューション「Azure Image Testing for Linux」のサービス提供を発表しました。 マイクロソフト、「Azure Image Testing for Linux」をサービス提供 https://japan.zdnet.com/article/35232982/ Google Chrome の最新バージョン「Chrome 137」に関する情報が公開されました。深刻度「High」の脆弱性が修正対応されています。 「Google Chrome」に8件の脆弱性、最大深刻度は「High」 https://forest.watch.impress.co.jp/docs/news/2016522.html ご覧いただきありがとうございます! この投稿はお役に立ちましたか? 役に立った 役に立たなかった 0人がこの投稿は役に立ったと言っています。 The post 【2025年5月】OSSサポートエンジニアが気になった!OSS最新ニュース first appeared on SIOS Tech. Lab .
はじめに こんにちは、サイオステクノロジーの小沼 俊治です。 SaaS 型のデータストリーミングプラットフォームの Confluent で、自然言語にて Confluent Cloud を操作できる Confluent MCP (Model Context Protocol) サーバーが GitHub に公開されました。この MCP サーバーを MCP ホストの Claude に組み込んで使ってみたので、設定方法を共有したいと思います。 confluentinc / mcp-confluent 何ができるかは、私たちが普段話す言葉で Claude から Confluent Cloud を操作するデモンストレーションをご覧ください。 本資料では、既に Confluent Cloud を利用されている方を対象としており、Confluent MCP サーバーを MCP ホストに設定する手順に焦点を当てて解説します。従って、Confluent Cloud の利用開始手順や設定、およびデータストリーミングプラットフォームの利用方法など、MCP サーバーの設定以外の内容については、本資料の範囲外とさせていただきます。 また、MCP についても既にネット上でたくさんのコンテンツが親切に説明されているので、本資料の範囲外とさせて頂きます。個人的には、KDDIアジャイル開発センター 御田さんが書かれた以下資料がお薦めです。 やさしいMCP入門 | 著者:御田 稔さま なお、ハンズオンで利用する機能は Claude の無料プランでお試し頂くことが可能ですが、便利さを感じて頂いたら是非ともアップグレードもご検討ください。 構成概要 筆者が動かした際の主な構成要素は以下の通りです。 Windows 10 Professional Claude desktop for Windows version 0.9.3 Windows PowerShell 5.1.19041.5737 Node.js v22.15.0 ハンズオンを構成する環境は以下の通りです。 MCP ホストは、Claude desktop for Windows のネイティブアプリ版を利用します。 MCP サーバーの構成における主な考慮事項は以下の通りです。 Confluent MCP サーバーは TypeScript で実装されているため、MCP サーバーの稼働環境は Node.js を導入します。 GitHub のリポジトリから取得した TypeScript のソースコードを JavaScript へトランスパイルして、Node.js 環境で MCP サーバーを実行します。 実行に当たっては、Confluent Cloud から API Key、API Secret などの情報を事前に収集します。 基礎環境の構築 Claude desktop for Windows インストール MCP ホストに Web 版ではなくアプリ版の Claude desktop for Windows を利用するため、以下公式手順を参考に Windows PC へインストールします。 デスクトップ版Claudeのインストール | Anthropicヘルプセンター Node.js インストール JavaScript の実行環境が必要なため、以下手順を参考に Windows PC へ Node.js をインストールします。 初期環境構築: Node.js on Windows Confluent Cloud から情報収集 本章では Confluent Cloud にログインして MCP サーバーの動作に必要な各種情報を収集します。 Confluent Cloud API を利用して Confluent Cloud を操作するため、API Key、および API Secret を取得します。 利用している Confluent Cloud の環境情報を収集します。 Confluent Cloud へログイン Confluent Cloud のログインページにアクセスしログインします。 Confluent Cloud Confluent Cloud にログインできたら、次の章に進みます。 「.env」ファイルの設定情報 Confluent MCP サーバーの起動には「 .env 」ファイルの設定が不可欠で、設定値について以下 GitHub のドキュメントに記載がありますが、多くの設定項目があるため、私自身も情報の収集には苦労しました。これまでに得たノウハウに基づき、ファイル作成に必要な項目の設定値や取得方法について解説します。 confluentinc/mcp-confluent Confluent MCP サーバーの環境構築する過程で、「 「.env」ファイルの作成 」でファイルを作成する際にこの解説を参考にしてください。 API_KEY / API_SECRET 「CONFLUENT_CLOUD_API_KEY」を始めとする各種 API Key と API Secret の取得に当たり、画面右上のハンバーガーメニューから「API Keys」を選択します。 API Key とペアーで API Secret を新たに発行するために、「+ Add API key」ボタンをクリックします。 自分自身のアカウントとして振る舞う API Key を発行するので「My account」を選び「Next」ボタンをクリックし、次ページ以降は発行する API Key に準じた章に従って進めます。 API Key と API Secret の発行画面では、画面を閉じると API Secret は二度と確認できなくなるため、必ずメモを取って控えます。控え終わったら「Complete」ボタンをクリックして発行プロセスを完了し、控えた API Key と API Secret はそれぞれの「 .env 」へ設定します。 CONFLUENT_CLOUD_API_KEY / API_SECRET 「Cloud resource management」を選択して「Next」ボタンをクリックします。 「Name」に任意の API Key 名を入力して「Next」ボタンをクリックして API Key と API Secret を発行します。 FLINK_API_KEY / API_SECRET 「Flink region」を選択して、MCP ホストから操作したい Flink の「Environment」「Cloud provide」「Region」を選択して「Next」ボタンをクリックします。 「Name」に任意の API Key 名を入力して「Next」ボタンをクリックして API Key と API Secret を発行します。 KAFKA_API_KEY / API_SECRET 「Kafka cluster」を選択して、MCP ホストから操作したい Kafka の「Environment」「Cluster」を選択して「Next」ボタンをクリックします。 「Name」に任意の API Key 名を入力して「Next」ボタンをクリックして API Key と API Secret を発行します。 SCHEMA_REGISTRY_API_KEY / API_SECRET 「Schema Registry」を選択して、MCP ホストから操作したい「Environment」「Schema Registry」を選択して「Next」ボタンをクリックします。 「Name」に任意の API Key 名を入力して「Next」ボタンをクリックして API Key と API Secret を発行します。 HTTP_HOST / HTTP_PORT HTTP_HOST と HTTP_PORT には、デフォルトの「 localhost 」「 3000 」を「 .env 」へ設定します。 BOOTSTRAP_SERVERS 画面の左ペインのメニューから「Environments」を選択し、MCP ホストから操作する Environment を選びます。 Cluster 一覧で MCP ホストから操作する Cluster を選びます。 画面の左ペインのメニューから「Cluster overview > Cluster settings」を選択し、Endpoints にある「Bootstrap server」の値を「 .env 」へ設定します。 CONFLUENT_CLOUD_REST_ENDPOINT 「 https://api.confluent.cloud 」を「 .env 」へ設定します。 KAFKA_ENV_ID MCP ホストから操作する Environment が選ばれている状態で、画面の右ペインの Environment details にある「ID」の値を「 .env 」へ設定します。 KAFKA_CLUSTER_ID MCP ホストから操作する Environment が選ばれている状態で、Cluster 一覧で MCP ホストから操作する Cluster を選びます。 画面の右ペインの情報一覧にある「Cluster ID」の値を「 .env 」へ設定します。 KAFKA_REST_ENDPOINT MCP ホストから操作する Cluster が選ばれている状態で、画面の左ペインのメニューから「Cluster overview > Cluster settings」を選択し、Endpoints にある「REST endpoint」の値を「 .env 」へ設定します。 FLINK_ORG_ID 画面右上のハンバーガーメニューから「Organization settings」を選択します。 Details にある「Organization ID」の値を「 .env 」へ設定します。 FLINK_COMPUTE_POOL_ID MCP ホストから操作する Environment が選ばれている状態で、画面の左ペインのメニューから「Flink」を選択し、MCP ホストから操作する Compute pool から「ID」の値を「 .env 」へ設定します。 FLINK_ENV_ID / FLINK_ENV_NAME Flink の Environment は、Kafka の Environment の ID を意味するため「 KAFKA_ENV_ID 」と同じ値を「 .env 」へ設定します。なお、FLINK_ENV_NAME には kafka の Environment に名付けられた名前を設定します。 FLINK_DATABASE_NAME Flink のデータベース名は、Kafka の Cluster ID を意味するため「 KAFKA_CLUSTER_ID 」と同じ値を「 .env 」へ設定します。 FLINK_REST_ENDPOINT MCP ホストから操作する Environment が選ばれている状態で、画面の左ペインのメニューから「Flink」を選択し、「Compute pools」タブを選択すると表示されている Compute pool 情報にある「Cloud & region」から cloud と region の値を控えます。(画面例では、「cloud = aws」「region = ap-northeast-1」が該当します) 「Endpoints」タブを選択し、Public Endpoints にある「Public endpoint」に書かれている書式を控えます。 控えた値、書式とプロトコルの「https://」使って得られたアドレスを「 .evn 」へ設定します 画面例では次のアドレスになります。:https://flink.ap-northeast-1.aws.confluent.cloud SCHEMA_REGISTRY_ENDPOINT MCP ホストから操作する Environment が選ばれている状態で、画面の左ペインのメニューから「Stream Governance > Schema Registry」を選択し、「Overview」タブを選択すると表示されている Endpoints にある「Public endpoint」の値を「 .env 」へ設定します。 Confluent MCP サーバーの環境設定 MCP サーバーの設定 Confluent MCP サーバーの GitHub リポジトリを取得して、MCP ホストの Claude と連携できるように準備します。 GitHub からリポジトリのダウンロード 「ドキュメント」フォルダ配下に、リポジトリを保存して管理する「 devlopment 」フォルダを作成します。 Path: C:\Users\{ユーザー名}\Documents\development\ {ユーザー名} はご利用中 PC のユーザー名に置き換えてください ブラウザで GitHub の「Confluent MCP Server」のリポジトリにアクセスします。 confluentinc/mcp-confluent リポジトリを ZIP ファイルで取得するため、緑色の「Code」ボタンをクリックすると表示するメニューから「Download ZIP」を選択して、名前を付けて保存するダイアログが表示されたら保存するフォルダを指定してダウンロードを開始します。 エクスプローラーでダウンロードした「 mcp-confluent-main.zip 」を右クリックして表示するメニューから「すべて展開…」を選択して ZIP ファイルを展開します。 ZIP ファイルから展開された「 mcp-confluent-main 」フォルダを、事前に準備した「 C:\Users\{ユーザー名}\Documents\development\ 」フォルダに移動するため、右クリックで表示するメニューの「切り取り」を選択して切り取ります。 なお、展開後のフォルダ構成が「 …\Donwloads\mcp-confluent-main\mcp-confluent-main\… 」のように「 mcp-confluent-main 」フォルダが二重に展開されている場合は、下段の「 mcp-confluent-main 」フォルダを移動対象にします。 事前に準備した「 C:\Users\{ユーザー名}\Documents\development\ 」フォルダをアクティブにして、右クリックのメニューから「貼り付け」を選択して「 mcp-confluent-main 」を移動します。 フォルダ名を「 mcp-confluent-main 」から末尾の main を消し「 mcp-confluent 」に変更します。 「 C:\Users\{ユーザー名}\Documents\development\mcp-confluent 」フォルダが準備できました。 Git コマンドがインストールされている PC 環境では、Windows PowerShell を使い以下コマンドでリポジトリを取得して頂くことで問題ありません。 PS C:\Users\...\development> git clone https://github.com/confluentinc/mcp-confluent.git 「.env」ファイルの作成 Confluent MCP サーバーの「 C:\Users\{ユーザー名}\Documents\development\mcp-confluent 」フォルダに「.env」ファイルを新規に作成します。 ファイルに設定する内容は以下 GitHub のドキュメントを確認してください。 confluentinc/mcp-confluent ファイル作成に必要な項目の設定値や取得方法について以下でも解説しています。 「.env」ファイルの設定情報 依存パッケージのインストールとトランスパイル Windows のスタートメニュー「W」セクションから「Windows PowerShell」を選択して起動します。 Window PowerShell ではコマンドラインベースで操作を行います。 リポジトリを保存したフォルダへ遷移します。 PS C:\Users\...> cd C:\Users\{ユーザー名}\Documents\development\mcp-confluent\ ソースコードが実行時に必要な依存パッケージをインストールします。 PS C:\Users\...\mcp-confluent> npm install added 726 packages, and audited 727 packages in 36s 127 packages are looking for funding run `npm fund` for details found 0 vulnerabilities 依存パッケージが格納された「 node_modules 」フォルダが新たに出来上がったことを確認します。 PS C:\Users\...\mcp-confluent> ls Mode LastWriteTime Length Name ---- ------------- ------ ---- d----- 2025/05/19 12:20 node_modules <--- 出来上がっている d----- 2025/05/19 12:15 src : ビルドして TypeScript のソースコードから Node.js で実行できる JavaScript のソースコードへトランスパイルします。 PS C:\Users\...\mcp-confluent> npm run build > @confluentinc/mcp-confluent@1.0.2 build > tsc && tsc-alias ビルド先の「 dist 」フォルダが新たに出来上がり、MCP サーバーの起動を担う「 dist\index.js 」が出来上がったことを確認します。 PS C:\Users\...\mcp-confluent> ls dist\ Mode LastWriteTime Length Name ---- ------------- ------ ---- : -a---- 2025/05/19 12:22 6036 index.js <--- 出来上がっている : Claude(MCP ホスト)の設定 Confluent MCP サーバーが Claude で利用できるように設定します。 連携先 MCP サーバーの設定 Windows のスタートメニュー「A」セクションから「Anthropic > Claude」を選択して起動します。 Claude が起動したら画面左上のハンバーガーメニューから「ファイル > 設定…」を選択します。 Confluent MCP サーバーが利用できるようにするために、「開発者」タブの「構成を編集」ボタンをクリックします。 エクスプローラーが立ち上がり、Claude の設定ファイルに該当する「 claude_desktop_config.json 」ファイルをダブルクリックしてエディターで開きます。 「 claude_desktop_config.json 」ファイルに Confluent MCP サーバーを利用するための設定を記載して保存します。 { "mcpServers": { "confluent": { "command": "node", "args": [ "C:\\Users\\{ユーザー名}\\Documents\\development\\mcp-confluent\\dist\\index.js", "--env-file", "C:\\Users\\{ユーザー名}\\Documents\\development\\mcp-confluent\\.env" ] } } } 「 claude_desktop_config.json 」ファイルの編集における主な考慮事項は以下の通りです。 「mcpServers > confluent > args」フィールドで主な考慮事項は以下の通りです。 node コマンドが MCP サーバーとして起動する「 index.js 」ファイルの Path を指定します。 「 --env-file 」オプションに付与する値は「 .env 」ファイルの Path を指定します。 「 .env 」ファイルは「 「.env」ファイルの設定情報 」章を参考に設定します。 Path にある「 {ユーザー名} 」は PC で利用しているユーザー名に置き換えます。 Path でフォルダの区切りを示す「\」は二重の「\\」で書く必要があります。 Claude の再起動で設定反映 変更した設定ファイルの適用には、ウィンドウの「×」ボタンで終了させるだけではなく、Claude のプロセスを完全に終了させる必要があるため、Windows タスクバーの画面右側のアイコン表示領域から Claude アイコン(トゲトゲのウニの様なデザイン)を探し、右クリックで表示されるメニューから「終了」を選択してプロセスを完全に終了させます。 Windows のスタートメニュー「A」セクションから「Anthropic > Claude」を選択して起動します。 Claude が起動したら画面中央のテキストボックス内の左下にある「検索とツール」アイコンをクリックします。表示されたメニューに、「confluent」の項目と右横に数字が表示されていたら、Confluent MCP サーバーは正常に設定できました。 Confluent MCP サーバーのデモ 私たちが普段話す言葉で Claude から Confluent Cloud を操作するデモンストレーションをご覧ください。 Confluentに新たに「mcp_users」トピックを作ってください。 Confluentに以下コネクターを新規に作成して、既に存在する「mcp_users」へテストデータを流してください。 – コネクター名: Connector_mcp_users – プラグイン: Sample Data – レコード書式: JSON_SR – スキーマ: Users なお、最終確認は自分でやるので、できあがり確認はしなくてよいです。 Confluentに「mcp_users」と同じスキーマ構造で新たに「mcp_users_mask」トピックを作り、Flinkを使って「mcp_users」トピックの全てのデータを流すSQLを発行してください。 但し、「gender」フィールドだけは登録されている半角英数字を正規表現で「*」へ置換してマスクしてください。 なお、最終確認は自分でやるので、できあがり確認はしなくてよいです。 まとめ Confluent MCP サーバーの組み込みとデモンストレーションはいかがでしたでしょうか。Confluent に限らず、ミドルウェアベンダー各社から設定を簡略化する MCP サーバーが提供されていますので、ぜひ色々と試してみていただければと思います。 ご覧いただきありがとうございます! この投稿はお役に立ちましたか? 役に立った 役に立たなかった 0人がこの投稿は役に立ったと言っています。 The post MCP を使って 自然言語で Confluent Cloud を操ってみた first appeared on SIOS Tech. Lab .
こんにちは。サイオステクノロジーの久保です。 突然ですが、ご自身のウェブサイトが現在どのような状況にあるのか、把握できていますか? どのページがよく閲覧されているのか、どこから訪問者が来ているのか、どのようなユーザーが関心を寄せているのか…。 これらを明らかにするのが「 アクセス解析 」です。 アクセス解析とは、ウェブサイトを訪れたユーザーの行動データを収集・分析することで、現状の把握や課題の発見、改善のための手がかりを得るための手法です。 アクセス解析でわかること たとえば、アクセス解析を行うことで、以下のような情報を得ることができます。 訪問者数 :どのくらいのユーザーがサイトを訪れているのか ページごとの閲覧数 :人気ページやあまり見られていないページの把握 流入元の特定 :検索エンジン、SNS、他サイトからのリンクなど、訪問のきっかけ ユーザー属性 :年齢層や地域など(※個人が特定できない範囲) ユーザー行動 :滞在時間、離脱ページ、ページ遷移など コンバージョン :購入やお問い合わせなど、目的の達成状況 こうした情報をもとに、ウェブサイトの改善策を検討したり、効果的なマーケティング戦略を立てたりすることが可能になります。 アクセス解析ツールの選定(Google Analytics) アクセス解析を行うためには、ツールの導入が必要です。 現在では、無料・有料を問わずさまざまなアクセス解析ツールが提供されています。 中でも多くのウェブサイトで採用されているのが、 Google Analytics(GA4) です。アカウントを作成すればすぐに使い始められる手軽さから、多くの人に選ばれてきました。 しかし近年、以下のような理由から別のツールを検討するケースも増えています。 仕様変更への戸惑い :GA4では、解析の軸がセッションからユーザーへと移り、従来のようなページ中心の分析がしづらくなった 予告なしのアップデート :仕様や操作画面の変更が突発的に行われることがある データのサンプリング :大量データを扱う際、すべてではなく一部のみを基にした分析となる場合があり、精緻な分析が難しい 保存期間の制限 :データの保存期間を自由に設定できない プライバシーへの懸念 :収集されたデータが大手プラットフォームのビッグデータとして利用されることへの不安 こうした課題を背景に、「もっと自由にデータを管理したい」「プライバシーを重視したい」「柔軟に拡張したい」という声が高まっています。 新たな選択肢「Matomo(マトモ)」 そのような中で注目を集めているのが Matomo というアクセス解析ツールです。 キャプション:Matomo demo画面 Matomoの概要 Matomoは LAMPサーバー(Linux, Apache, MySQL, PHP) 上で動作するオープンソースの無料ツールです。 2010年に「Piwik」として誕生し、2018年に「Matomo」へ名称変更。2025年5月時点で最新版は Matomo 5.2.3 です。 現在、190か国・100万以上のウェブサイト(国連やアムネスティなどの国際機関を含む)で採用され、その信頼性が認められています。 Matomoの主な特徴 Matomoが多くのユーザーに支持されるのには、明確な理由があります。ここでは、その主な特徴を詳しく見ていきましょう。 データの完全所有権  すべての分析データはユーザー自身が用意するデータベースにのみ保存されます。  外部プラットフォームに共有されることはなく、安心して活用できます。 プライバシー保護に配慮   GDPR(EU)、HIPAA(米国)、CCPA(カリフォルニア)、LGPD(ブラジル)、PECR(英国) など  世界各国の厳格なプライバシー法に準拠する設定が可能です。 拡張可能な機能  有償・無償のプラグインがあり、より高度なレポー確認ト作成や自社要件に応じた機能拡張ができます。 無料のオープンソース  ライセンス費用は不要。ソフトウェアの実行・共有・調査・変更が自由で、コストを抑えながら高機能環境を構築できます。 柔軟性と高いカスタマイズ性  250以上の設定項目があり、多くはデフォルトで対応可能。  さらにHTTP APIを活用してカスタムレポートの自動作成もできます。 データサンプリングなし  大規模解析の場合でも、GAのような推測に基づくサンプリングは行わず、「すべてのデータ」で正確なレポートを作成できます。 GDPR対応支援   データ匿名化   トラッキングのオプトアウト提供   EU圏内でのデータ保存  違反時に最大年間収益4%の罰金リスクがあるGDPR対応も支援します。 アクセスログファイル解析も可能  トラッキングコードを設置できない場合でも、Apache/nginx/IISなど主要サーバーのログファイルを活用した解析が可能。  過去データも遡って分析できる点が特徴です(ただし一部指標は対応不可)。 より深くアクセス解析とMatomoを知るために もしアクセス解析とMatomoに興味を持ち、さらに詳しく知りたい場合は サイオステクノロジー Financial & Unique SI Service Line が発信する Note記事 をぜひご覧ください。 筆者の私自身、以下の記事を活用して「 ウェブ解析士 」の資格を取得しました! 「初心者のためのやさしいアクセス解析入門 第1章」 https://note.com/sti_fusl/m/me4d2bc51e095 「初心者のためのやさしいアクセス解析入門 第2章」 https://note.com/sti_fusl/m/mb94d1a928bde 「Matomoな日々」 https://note.com/sti_fusl/m/m4ba37fa0eb27 サイオステクノロジーのMatomoサポートサービス また、サイオステクノロジーでは2014年からMatomo国内向けテクニカルサポートサービスを提供しています。 https://sios.jp/lp/matomo/ このサービスを通じて、Matomoの導入支援や運用に関する専門的なサポートを受けることができます。 Matomoは、データの所有権、プライバシー保護、そして高機能な分析機能を兼ね備えた、ウェブサイト運営者にとって強力な味方となるでしょう。ぜひこの機会に、Matomoの導入を検討してみてはいかがでしょうか。 ご覧いただきありがとうございます! この投稿はお役に立ちましたか? 役に立った 役に立たなかった 0人がこの投稿は役に立ったと言っています。 The post アクセス解析、やるならこれ!初心者にもわかりやすいMatomoのすすめ first appeared on SIOS Tech. Lab .
こんにちは、サイオステクノロジーの佐藤 陽です。 今回は少しニッチなところで、EntraIDのアプリケーションマニフェストの概要をご紹介したいと思います。 本当に記事にしたいのはこのマニフェスト部分関連でハマったポイントの解決方法なのですが その解決方法を理解するにはマニフェストの概要を知っておく必要があるため、まずはこちらを紹介していきたいと思います。 はじめに Azureを利用したアプリケーションにおいて認証認可の仕組みを実現する際、EntraIDを使うことも多いかと思います。 そのEntraIDには「マニフェスト」と呼ばれる設定項目があります。 今回そのマニフェストに関してどういったものかをご紹介したいと思います。 EntraIDのアプリケーションマニフェストとは 認証認可の仕組みを実現する際には、EntraID上でアプリケーションを作成し、Microsoft Authentication Library (MSAL) などを利用することが多いかと思います。 MSALを利用することで、ClientIDなどを与えるだけで簡単にログイン画面などの実装が可能となります。 今回触れる「アプリケーションマニフェスト」は、そのEntraID上に作成したアプリケーションの要素になります。 実際、アプリケーションのページを表示すると、管理のブレードの一番下に マニフェスト というブレードが存在します。 そしてこの項目を選択すると、何やらjson形式のテキストデータが表示されることがわかるかと思います。 これが今回ご紹介するマニフェストの内容になります。 どういった内容が記載されているのか マニフェストのテキストを確認すると、何かの設定パラメータのようにも見えます。 実は、このマニフェストにはアプリケーションの設定にかかわる全てのことが記載されています。 つまり、Azure Portal上において認証の設定や、トークン構成などを行うことが多いかと思いますが、 それらの設定内容がすべてこのマニフェストにテキストとして反映されます。 (逆に、マニフェストを修正することでしか設定できない項目もいくつか存在します。) 例えば以下のようなものが含まれます。 Property 内容 appId アプリケーションのクライアントID groupMembershipClaims ユーザーが所属するグループ情報をクレームに含めるか、およびそのグループの種別の設定 signInAudience サインインできるユーザーのテナント(単一テナントorマルチテナントなど) 設定を変更する際には、AzurePortal上から設定してもらえればこのマニフェストの値も書き換わりますし、一方でマニフェストを直接書き換えることでもPortal上の設定を変更することが可能です。 修正方法 マニフェストの情報を直接Azure Portalから修正することはできません。 修正するためには、一度ダウンロードし、ローカルで修正したのちにアップロードします。 試しに一度ダウンロードし、空欄であった”identifierUris”の値を以下のように設定します。 "identifierUris": [ "api://058cae36-9e85-4e29-b9e1-658943948f40" ], そしてアップロード後にAPIの公開のブレードを確認すると 新規にアプリケーションID URIが発行されていることが確認できました。 では試しに、変更してはいけなさそうな値を変えてみたいと思います。 アプリケーション作成時に自動で割り当てられる appId の値を修正し、アップロードします。 するとさすがに以下のように怒られてしまいました。 確かに ドキュメント を読んでも、appIdに関しては It’s a not nullable and read-only attribute. との記載があるので修正はできないようです。 マニフェストの使われ方 このマニフェストがアプリケーション設定そのものだということは理解いただけたかと思います。 MSALなどでこのアプリケーションに対してアクセスする際は、このマニフェストの内容に基づいて処理が行われます。 例えば、クライアントに返されるアクセストークンのクレームの項目などがそれに該当します。 そのため、認証認可の仕組みを実現する際には「どこのアプリケーションマニフェストに基づいて処理が行われているか」という意識を持つことが大切です。 まとめ 今回はEntraIDのアプリケーションにおけるマニフェストという概念をご紹介しました。 一言でいえば、EntraIDのすべての設定項目をjson形式のオブジェクトであらわしたものになります。 そしてこのマニフェストに沿って認証認可の処理が実現されます。 なぜこのマニフェストについてご紹介したか、ですが 最近EntraID周りの実装でハマったポイントがあり、それを解消するためにこのマニフェストの概念の理解が必要でした。 そのため次回はそのハマりポイントの解消方法についてご紹介します。 ではまた! 参考ページ https://learn.microsoft.com/en-us/entra/identity-platform/reference-microsoft-graph-app-manifest ご覧いただきありがとうございます! この投稿はお役に立ちましたか? 役に立った 役に立たなかった 0人がこの投稿は役に立ったと言っています。 The post 【Azure】EntraIDにおけるアプリケーションマニフェストとは? first appeared on SIOS Tech. Lab .
サイオステクノロジーの橋本 です。 個人的に max_wal_size と wal_keep_size の役割を忘れたり、混同しがちなので、 備忘のためブログを書きます。 パラメータの説明 max_wal_size PostgreSQL 9.5 から追加されているパラメータです。 役割は 2 つあります。 ・保存される WAL ファイルサイズの上限値設定 (ソフトリミット) ・CHECKPOINT の実行条件の制御 wal_keep_size PostgreSQL 13から登場したレプリケーション用のパラメータです。 以前は wal_keep_segments という名前でした。 削除も再利用もされることがなく保存が保証される WAL ファイルサイズを定義します。 チューニング時の指針について max_wal_size max_wal_size の重要な役割として CHECKPOINT の実行条件の制御があります。 ※CHECKPOINT の仕組みは複雑で説明すると脱線するので触れません  CHECKPOINT が発生すると大きな DISK I/O が発生します。 CHECKPOINT が発生する条件は以下の 2 つです (or 条件) ・ checkpoint_timeout 秒が経過する ・max_wal_size に達する CHECKPOINT の実行は大きな DISK I/O が伴うため可能な限り発生を抑止したいです。 例えば checkpoint_timeout = 60min と設定すれば CHECKPOINT は一時間に一回の実行に抑えることが可能です。 ※CHECKPOINT の発生間隔が長くなるとクラッシュリカバリに要する時間が長くなるデメリットがあります。 この場合、CHECKPOINT は一時間に一回の実行としたいという思惑があるわけです。 次にようやく max_wal_size のチューニング観点です。 CHECKPOINT は max_wal_size で指定したサイズ分だけ WAL ファイルが生成されても実行されます。 一時間に 10 GB の更新 (WAL ファイルの生成) がされるシステムは 最低限 max_wal_size の値も 10 GB と設定する必要があります。 ただし、厳密に max_wal_size で指定したサイズ分の更新があった場合に CHECKPOINT 発生するわけではなく 多少の誤差が生じます。 この点は運用していく中でチューニングが必要です。 wal_keep_size このパラメータは最低限の保存する WAL ファイルサイズを指定します。 主にストリーミングレプリケーションの時に必要となるパラメータです。 例えばネットワークメンテナンスやパッチ適用などでレプリケーションが最長 3 時間途絶える可能性がある環境があるとします。 3 時間で生成される WAL ファイルサイズを wal_keep_size を指定する必要があります。 つまり一時間に 10 GB の更新 (WAL ファイルの生成) がされるシステムは 30 GB を wal_keep_size に指定する必要があります。 wal_keep_size の値は max_wal_size の値より大きく手も小さくても問題ありません。 以下の場合は WAL ファイルは最低 30 GB を保持してくれます。 max_wal_size = 10GB wal_keep_size = 30GB この例からもわかるように max_wal_size はあくまでソフトリミットとなります。 max_wal_size を超えて WAL ファイルを保持する例として他には以下があります。  archive_command の失敗  レプリケーションスロットに基づく保存 ただし、レプリケーションスロットを利用している場合は wal_keep_size を設定する必要は薄くなります。 本記事がチューニングを検討しているかたの助けになれば幸いです。 ご覧いただきありがとうございます! この投稿はお役に立ちましたか? 役に立った 役に立たなかった 0人がこの投稿は役に立ったと言っています。 The post PostgreSQLのWAL管理 max_wal_sizeとwal_keep_sizeの役割とチューニング first appeared on SIOS Tech. Lab .
こんにちは、OSSよろず相談室の神崎です。 RHEL 10 がリリースされたということで、同梱版の OSS のバージョンについて軽くまとめていきたいと思います。 Apache HTTP Server httpd-2.4.63-1.el10 Apache Tomcat tomcat-10.1.8-2.el10 PostgreSQL postgresql-16.8-1.el10_0 MariaDB mariadb-10.11.11-1.el10 MySQL mysql8.4-8.4.4-2.el10 Nginx nginx-1.26.3-1.el10 BIND bind-9.18.33-3.el10 FreeRADIUS freeradius-3.2.5-4.el10 OpenLDAP openldap-2.6.8-3.el10 OpenSSH openssh-2.6.8-3.el10 OpenSSL openssl-3.2.2-16.el10 Squid squid-6.10-5.el10 Samba samba-4.21.3-102.el10 Postfix postfix-3.8.5-8.el10 Dovecot dovecot-2.3.21-16.el10 vsftpd vsftpd-3.0.5-9.el10 Ansible ansible-core-2.16.14-1.el10 Net-SNMP net-snmp-5.9.4-15.el10_0 Git git-2.47.1-1.el10 Unbound unbound-1.20.0-9.el10 PHP php-8.3.15-1.el10 Perl perl-5.40.1-512.el10 Python python3-3.12.9-1.el10 Ruby ruby-3.3.7-9.el10 Kernel kernel-6.12.0-55.9.1.el10_0 参考 Red Hat Enterprise Linux パッケージ https://access.redhat.com/downloads/content/479/ver=/rhel—10/10.0/x86_64/packages ※Red Hat社の有料ポータルログインIDが必要です ご覧いただきありがとうございます! この投稿はお役に立ちましたか? 役に立った 役に立たなかった 0人がこの投稿は役に立ったと言っています。 The post RHEL 10 同梱版主要 OSS のバージョンまとめ first appeared on SIOS Tech. Lab .
はじめに こんにちは、サイオステクノロジーの小沼 俊治です。 以前の記事ではベンダーが公開されている MCP サーバーを組み込んで利用してみましたが、 MCP を使って 自然言語で Kong Konnect を操ってみた | SIOS Tech. Lab もう少し仕組みを理解してみたくなったので、独自で考えた機能を搭載した 「オリジナルのちょっと便利な MCP サーバー」 を自作してみることにしました。 実際に MCP サーバーの実装に取り組んで、色々試していく過程でやっと手順が理解でき、その経験から得られたナレッジをまとめましたので、これから MCP サーバーを作りたい皆さんの力になれたら嬉しいです。 何ができるのかは、私たちが普段話す言葉で Claude から天気予報を教えてもらう驚きのデモンストレーションをご覧ください。 なお、MCP については既にネット上でたくさんのコンテンツが親切に説明されているので、本資料の範囲外とさせて頂きます。個人的には、KDDIアジャイル開発センター 御田さんが書かれた以下資料がお薦めです。 やさしいMCP入門 | 著者:御田 稔さま 構成概要 筆者が動かした際の主な構成要素は以下の通りです。 Windows 10 Professional Claude desktop for Windows version 0.9.3 Windows PowerShell 5.1.19041.5737 Node.js v22.15.0 Visual Studio Code version 1.99.3 ハンズオンを構成する環境は以下の通りです。 MCP ホストは、Claude desktop for Windows のネイティブアプリ版を利用します。 MCP サーバーの構成における主な考慮事項は以下の通りです。 MCP サーバーを稼働させる環境は Node.js 環境を選んでいます。 ソースコードは TypeScript 言語で実装します。 MCP サーバーを実行するために、TypeScript で実装したソースコードをトランスパイルして JavaScript のソースコードを生成します。 実装したツールは、MCP Inspector を起動してブラウザから動作確認します。 気象庁の API を利用した天気予報を取得する MCP サーバーのツールを実装します。 MCPサーバーとして実装する4つのツール仕様は以下の通りです。 「get_officecode_from_prefecture」は、都道府県名からエリアコード API を用いて、天気予報 API で必要な気象台コードを取得します。 「get_officecode_from_regionname」は、地域名からエリアコード API を用いて、天気予報 API で必要な気象台コードを取得します。 「get_officecode_from_cityname」は、地区町村名からエリアコード API を用いて、天気予報 API で必要な気象台コードを取得します。 「get_weather_forecast_from_areacode」は、気象台コードから天気予報 API を用いて天気予報を取得します。 ハンズオンの手順で作成する MCP サーバーの設定ファイルやソースコードは、以下の GitHub リポジトリで公開しています。手順と合わせてご確認ください。 hands-on-mcp-sios-apisl @ GitHub v0.0.0 : MCP サーバーのスケルトン v1.0.0 : オリジナルのちょっと便利な MCP サーバーを実装 基礎環境の構築 Claude desktop for Windows インストール MCP ホストに Web 版ではなくアプリ版の Claude desktop for Windows を利用するため、以下公式手順を参考に Windows PC へインストールします。 デスクトップ版Claudeのインストール | Anthropicヘルプセンター Node.js インストール JavaScript の実行環境が必要なため、以下手順を参考に Windows PC へ Node.js をインストールします。 初期環境構築: Node.js on Windows Visual Studio Code インストール Windows 環境でソースコードや設定ファイルの編集をするためのエディターとして Visual Studio Code (VS Code) のインストールをお薦めします。インストール手順については、ネット上でたくさんのコンテンツが親切に説明されているので、本資料の範囲外とさせて頂きます。 Visual Studio Code インストール – Google 検索 なお、VS Code 以外のお気に入りのエディターを利用される場合は、文中の VS Code をご利用のエディターに置き換えて読み進めてください。 MCP サーバーのスケルトンを作成 プロジェクトの新規作成 Windows のスタートメニュー「W」セクションから「Windows PowerShell」を選択して起動します。 Window PowerShell ではコマンドラインベースで操作を行います。 「ドキュメント」フォルダ配下に、MCP サーバー開発用のプロジェクトフォルダ「 devlopment\hands-on-mcp-sios-apisl 」を作成して遷移します。 PS C:\Users\...> cd ~\Documents PS C:\Users\...\Documents> mkdir -p development\hands-on-mcp-sios-apisl PS C:\Users\...\Documents> cd development\hands-on-mcp-sios-apisl プロジェクトの情報管理を担う「 package.json 」ファイルを生成します。 PS C:\Users\...\hands-on-mcp-sios-apisl> npm init -y MCP サーバーの実装に必要な依存パッケージを「 package.json 」ファイルに定義します。 PS C:\Users\...\hands-on-mcp-sios-apisl> npm install @modelcontextprotocol/sdk zod PS C:\Users\...\hands-on-mcp-sios-apisl> npm install -D typescript @types/node TypeScript から JavaScript へのトランスパイルに必要な、コンパイルオプションを管理する「 tsconfig.json 」ファイルを生成します。 PS C:\Users\...\hands-on-mcp-sios-apisl> npx tsc --init プロジェクトの初期化状態でファイルが揃いました。 PS C:\Users\...\hands-on-mcp-sios-apisl> ls Mode LastWriteTime Length Name ---- ------------- ------ ---- d----- 2025/05/07 10:00 node_modules -a---- 2025/05/07 10:00 36094 package-lock.json -a---- 2025/05/07 10:00 399 package.json -a---- 2025/05/07 10:00 12926 tsconfig.json ビルド環境の整備 TypeScript から JavaScript へソースコードをトランスパイルできるように設定ファイルを整えため VS Code を起動します。 PS C:\Users\...\hands-on-mcp-sios-apisl> code . プロジェクトフォルダをルートに VS Code が起動しました。 まず始めに「 package.json 」ファイルを VS Code で開き、ビルド用のコマンドを追記します。以下のファイルイメージで、各行の左端に記載されている「+」は、ファイルに追加する行を表現しているため、「+」自体は実際に追記しません。 : "main": "index.js", "scripts": { + "build": "tsc", "test": "echo \"Error: no test specified\" && exit 1" }, "repository": { : 設定ファイルの編集における主な考慮事項は以下の通りです。 「build」タスクでは、tsc コマンドが「 tsconfig.json 」ファイルに従って TypeScript から JavaScript へソースコードをトランスパイルします。 編集後のできあがりは以下を参考にしてください。 package.json 次に「 tsconfig.json 」ファイルを VS Code で開き、以下の内容に書き換えます。 { "compilerOptions": { "target": "ES2023", "module": "Node16", "moduleResolution": "Node16", "outDir": "./build", "rootDir": "./src", "strict": true, "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true }, "include": ["src/**/*"], "exclude": ["node_modules"] } 設定ファイルの編集における主な考慮事項は以下の通りです。 「target」「module」「moduleResolution」には現時点で選べる最新の値を指定します。 「outDir」には、tsc コマンドでビルドしたソースコードを出力するフォルダを指定します。 「rootDir」には、tsc コマンドでビルドするソースコードが存在するフォルダを指定します。 編集後のできあがりは以下を参考にしてください。 tsconfig.json 初期状態の index.ts を作成 Windows PowerShell に戻り、MCP サーバーが起動できるようにソースコードの実装と管理をする「 src 」フォルダを作成します。 PS C:\Users\...\hands-on-mcp-sios-apisl> mkdir src VS Code に戻り、MCP サーバーを起動するスケルトン状態の「 src\index.ts 」を、以下のソースコードをコピペして新規に作成します。 import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { z } from "zod"; const server = new McpServer({ name: "mcp-sios-apisl-demo", version: "1.0.0", }); async function main() { const transport = new StdioServerTransport(); await server.connect(transport); console.error("MCP Server running on stdio"); } main().catch((error) => { console.error("Fatal error in main():", error); process.exit(1); }); 編集後のできあがりは以下を参考にしてください。 src/index.ts ビルドしてトランスパイル 「 src\index.ts 」の作成が完了したら、スケルトンのできあがりを動作確認するために、ビルドして TypeScript から JavaScript のソースコードを生成します。 PS C:\Users\...\hands-on-mcp-sios-apisl> npm run build > hands-on-mcp-sios-apisl@1.0.0 buildw > tsc ビルドが完了すると「 build\index.js 」ができあがり、プロジェクト全体としては以下の構成となります。(「 node_modele 」フォルダ配下の記載は省略します) PS C:\Users\...\hands-on-mcp-sios-apisl> tree /f C:. │ package-lock.json │ package.json │ tsconfig.json │ ├─build │ index.js │ ├─node_modules │ : │ (省略) │ └─src index.ts これまでの手順で作成したプロジェクトファイルを GitHub のリポジトリで管理する際は、以下にご留意ください。 「 package-lock.json 」「 package.json 」「 tsconfig.json 」のファイルを構成管理します。 「 src 」フォルダ、およびその配下のソースコードを構成管理します。 「 node_module 」フォルダは構成管理の対象外とし、必要な依存パッケージは「 npm install 」コマンドで npm レジストリから随時ダウンロードします。 「 build 」フォルダは構成管理の対象外とし、必要な際にビルドして JavaScript のソースコードを生成します。 MCP Inspector で動作確認 MCP サーバーのスケルトンができあがったことを確認するために、MCP Inspector を起動してログに表示される URL へブラウザでアクセスします。 (以下のコンソールイメージでは「http://127.0.0.1:6274」がアクセス先の URL です) PS C:\Users\...\hands-on-mcp-sios-apisl> npx @modelcontextprotocol/inspector node build/index.js Starting MCP inspector... Proxy server listening on port 6277 MCP Inspector is up and running at http://127.0.0.1:6274 ブラウザで MCP Inspector へアクセスしたら画面左中央部の「Connect」ボタンをクリックして MCP サーバーに接続します。 現時点の MCP サーバーはツールが一つもないので、画面中央には「The connected server does not support any MCP capabilities」のメッセージが表示されるだけです。スケルトンは無事にできあがりました。 オリジナルの MCP サーバーを実装 スケルトンにツールを実装 MCP サーバーのスケルトンに4つのツールを構造化して実装するので、Windows PowerShell で「 src 」フォルダ配下にサブフォルダを作成します。 PS C:\Users\...\hands-on-mcp-sios-apisl> mkdir src\interfaces PS C:\Users\...\hands-on-mcp-sios-apisl> mkdir src\services 以下の構成でフォルダとファイルが存在します。 PS C:\Users\...\hands-on-mcp-sios-apisl> tree /f src C:\USERS\{ユーザー名}\DOCUMENTS\DEVELOPMENT\HANDS-ON-MCP-SIOS-APISL\SRC │ index.ts │ ├─interfaces └─services VS Code をアクティブにして、新規に「 src\interfaces\areaApi.ts 」ファイルを作成してインターフェースを定義します。 export interface OfficeInfo { name: string; officeName: string; children: string[]; } export interface OfficeCodes { [key: string]: OfficeInfo; } export interface AreaInfo { name: string; parent: string; } export interface AreaCodes { [key: string]: AreaInfo; } export interface AreaApiData { centers: OfficeCodes; offices: OfficeCodes; class10s: AreaCodes; class15s: AreaCodes; class20s: AreaCodes; } 編集後のできあがりは以下を参考にしてください。 src/interfaces/areaApi.ts 続いて新規に「 src\services\areaWeatherForecast.ts 」ファイルを作成し、ツールから呼び出される関数を実装します。 import { AreaApiData, OfficeCodes, AreaCodes } from "../interfaces/areaApi.js"; const URL_AREA = "https://www.jma.go.jp/bosai/common/const/area.json"; const URL_WEATHER_FORECAST = "https://www.jma.go.jp/bosai/forecast/data/forecast/{areaCode}.json"; export async function get_officecode_from_prefecture(prefecture: string): Promise<string> { // console.error(">>> areaName: ", areaName); const json: AreaApiData = await fetchAreaDataFromJmaAreaAPI(); let officeCode = retrieveOfficeCodeFromPrefecture(json, prefecture); return officeCode; } export async function get_officecode_from_regionname(regionName: string): Promise<string> { const json: AreaApiData = await fetchAreaDataFromJmaAreaAPI(); let officeName = retrieveOfficeNameFromRegionName(json, regionName); let officeCode = retrieveOfficeCodeFromOfficeName(json, officeName); return officeCode; } export async function get_officecode_from_cityname(cityName: string): Promise<string> { const json: AreaApiData = await fetchAreaDataFromJmaAreaAPI(); let areaCode = retrieveAreaCodeFromCityName(json, cityName); let subOfficeCode = retrieveSubOfficeCodeFromAreaCode(json, areaCode); let officeCode = retrieveOfficeCodeFromSubOfficeCode(json, subOfficeCode); return officeCode; } export async function get_weather_forecast_from_officecode(areaCode: string): Promise<string> { const resp = await fetch(URL_WEATHER_FORECAST.replace("{areaCode}", areaCode)); const body = await resp.text(); return body; } async function fetchAreaDataFromJmaAreaAPI(): Promise<AreaApiData> { const resp = await fetch(URL_AREA); const body = await resp.text(); return JSON.parse(body); } function retrieveOfficeNameFromRegionName(json: AreaApiData, regionName: string): string { regionName = regionName.trim(); if (regionName === "") { return ""; } const officeCodes: OfficeCodes = json.centers; for (const code in officeCodes) { const target = officeCodes[code].name; if (target === regionName) { return officeCodes[code].officeName; } } for (const code in officeCodes) { const target = officeCodes[code].name; if (target.startsWith(regionName)) { return officeCodes[code].officeName;; } } return ""; } function retrieveOfficeCodeFromPrefecture(json: AreaApiData, prefecture: string): string { prefecture = prefecture.trim(); if (prefecture === "") { return ""; } const officeCodes: OfficeCodes = json.offices; for (const code in officeCodes) { const target = officeCodes[code].name; if (target === prefecture) { return code; } } for (const code in officeCodes) { const target = officeCodes[code].name; if (target.startsWith(prefecture)) { return code; } } return ""; } function retrieveOfficeCodeFromOfficeName(json: AreaApiData, officeName: string): string { officeName = officeName.trim(); if (officeName === "") { return ""; } const officeCodes: OfficeCodes = json.offices; for (const code in officeCodes) { const target = officeCodes[code].officeName; if (target === officeName) { return code; } } for (const code in officeCodes) { const target = officeCodes[code].officeName; if (target.startsWith(officeName)) { return code; } } return ""; } function retrieveAreaCodeFromCityName(json: AreaApiData, cityName: string): string { cityName = cityName.trim(); if (cityName === "") { return ""; } const areaCodes: AreaCodes = json.class20s; for (const code in areaCodes) { const target = areaCodes[code].name; if (target === cityName) { return areaCodes[code].parent; } } for (const code in areaCodes) { const target = areaCodes[code].name; if (target.startsWith(cityName)) { return areaCodes[code].parent; } } return ""; } function retrieveSubOfficeCodeFromAreaCode(json: AreaApiData, areaCode: string): string { areaCode = areaCode.trim(); if (areaCode === "") { return ""; } const areaCodes: AreaCodes = json.class15s; for (const code in areaCodes) { if (code === areaCode) { return areaCodes[code].parent; } } return ""; } function retrieveOfficeCodeFromSubOfficeCode(json: AreaApiData, subOfficeCode: string): string { subOfficeCode = subOfficeCode.trim(); if (subOfficeCode === "") { return ""; } const officeCodes: OfficeCodes = json.offices; for (const code in officeCodes) { const target = officeCodes[code].children; if (target.includes(subOfficeCode)) { return code; } } return ""; } 編集後のできあがりは以下を参考にしてください。 src/services/areaWeatherForecast.ts 既に存在している MCP サーバーのスケルトン「 src\index.ts 」へツールを実装するために2か所を改修します。 1か所目の改修は以下の通りです。行頭の「+」は追加する行を指示するイメージになります。 : import { z } from "zod"; + import { get_officecode_from_prefecture, get_officecode_from_regionname, get_officecode_from_cityname, get_weather_forecast_from_officecode } from "./services/areaWeatherForecast.js"; const server = new McpServer({ : ソースコードの改修における主な考慮事項は以下の通りです。 import 文にて、ツールが呼び出す関数を読み込みます。 2か所目の改修は以下の通りです。 : const server = new McpServer({ name: "mcp-example", version: "1.0.0", }); + server.tool( + "get_officecode_from_prefecture", + "Get an office code from a prefecture before getting a weather forecast.", + {prefecture: z.string().describe("Prefecture")}, + async ({prefecture}) => { + const areaCode = await get_officecode_from_prefecture(prefecture); + return {content: [{type: "text", text: areaCode}]}; + } + ); + + server.tool( + "get_officecode_from_regionname", + "Get an office code from a region name before getting a weather forecast.", + {regionName: z.string().describe("Region name")}, + async ({regionName}) => { + const areaCode = await get_officecode_from_regionname(regionName); + return {content: [{type: "text", text: areaCode}]}; + } + ); + + server.tool( + "get_officecode_from_cityname", + "Get an office code from a city name before getting a weather forecast.", + {cityName: z.string().describe("City name")}, + async ({cityName}) => { + const areaCode = await get_officecode_from_cityname(cityName); + return {content: [{type: "text", text: areaCode}]}; + } + ); + + server.tool( + "get_weather_forecast_from_officecode", + "Get a weather forecast from an office code.", + {officeCode: z.string().describe("Office code")}, + async ({officeCode}) => { + const body = await get_weather_forecast_from_officecode(officeCode); + return {content: [{type: "text", text: body}]}; + } + ); async function main() { : ソースコードの改修における主な考慮事項は以下の通りです。 4つのツールの公開と、ビジネスロジックが実装された関数を呼び出します。 編集後のできあがりは以下を参考にしてください。 src/index.ts ここまでの手順で以下のフォルダとファイル構成ができあがります。 PS C:\Users\...\hands-on-mcp-sios-apisl> tree /f src C:\USERS\{ユーザー名}\DOCUMENTS\DEVELOPMENT\HANDS-ON-MCP-SIOS-APISL\SRC │ index.ts │ ├─interfaces │ area.ts │ └─services areaWeatherForecast.ts ビルドしてトランスパイル ソースコードが揃ったので、実装したツールのできあがりを動作確認するために、ビルドして TypeScript から JavaScript のソースコードを生成します。 PS C:\Users\...\hands-on-mcp-sios-apisl> npm run build > hands-on-mcp-sios-apisl@1.0.0 buildw > tsc MCP Inspector で動作確認 MCP サーバーに実装したツールを動作確認するため、MCP Inspector を起動し、ログに表示される URL へブラウザでアクセスします。ここでは、実装した4つのツールの中から、「get_officecode_from_prefecture」と「get_weather_forecast_from_officecode」の2つのテスト手順を説明します。 PS C:\Users\...\hands-on-mcp-sios-apisl> npx @modelcontextprotocol/inspector node build/index.js Starting MCP inspector... Proxy server listening on port 6277 MCP Inspector is up and running at http://127.0.0.1:6274 ブラウザで MCP Inspector へアクセスしたら画面左中央部の「Connect」ボタンをクリックして MCP サーバーに接続します。 MCP サーバーにツールが搭載されているので、画面中央部に Tools 関連の情報が表示されており、「List Tools」ボタンをクリックすると接続した MCP サーバーが持っているツール一覧が表示されます。 表示されているツール一覧から2つのツールをテストします。 まずは「get_officecode_from_prefecture」ツールを選んでテストを実施します。 テスト手順は以下の通りです。 List Tools から「get_officecode_from_prefecture」ツールをクリックします。 prefecture のテキストボックスに都道府県名を入力します。 「Run Tool」ボタンをクリックすると都道府県名に該当する気象台コードが表示され、正常にツールが動作することを確認できます。 次に「get_weather_forecast_from_officecode」ツールをテストします。 テスト手順は以下の通りです。 List Tools から「get_weather_forecast_from_officecode」ツールをクリックします。 officeCode のテキストボックスに先に実施した「get_officecode_from_prefecture」ツールのテストで得られた気象台コードを入力します。 「Run Tool」ボタンをクリックすると気象台コードに該当する天気予報が表示され、正常にツールが動作することを確認できます。 Claude(MCP ホスト)に組み込み 連携先 MCP サーバーの設定 オリジナルの MCP サーバーが Claude で利用できるように設定ファイルを編集するため、Windows のスタートメニュー「A」セクションから「Anthropic > Claude」を選択して起動します。 Claude が起動したら画面左上のハンバーガーメニューから「ファイル > 設定…」を選択します。 オリジナルの MCP サーバーが利用できるようにするために、「開発者」タブの「構成を編集」ボタンをクリックします。 エクスプローラーが立ち上がり、Claude の設定ファイルに該当する「 claude_desktop_config.json 」ファイルをダブルクリックしてエディターで開きます。 「 claude_desktop_config.json 」ファイルにオリジナルの MCP サーバーを利用するための設定を書いて保存します。 { "mcpServers": { "mcp-sios-apisl-demo": { "command": "node", "args": [ "C:\\Users\\{ユーザー名}\\Documents\\development\\hands-on-mcp-sios-apisl\\build\\index.js" ] } } } 「 claude_desktop_config.json 」ファイルの編集における主な考慮事項は以下の通りです。 「mcpServers > mcp-sios-apisl-demo > args」フィールドで主な考慮事項は以下の通りです。 node コマンドが MCP サーバーとして起動する「 index.js 」の Path を指定します。 Path にある「 {ユーザー名} 」は PC で利用しているユーザー名に置き換えます。 Path でフォルダの区切りを示す「 \ 」は二重の「 \\ 」で書く必要があります。 Claude の再起動で設定反映 変更した設定ファイルの適用に、ウィンドウの「×」ボタンで終了させるだけではなく、Claude のプロセスを完全に終了させる必要があるため、Windows タスクバーの画面右側のアイコン表示領域から Claude アイコン(トゲトゲのウニの様なデザイン)を探し、右クリックで表示されるメニューから「終了」を選択してプロセスを完全に終了させます。 Windows のスタートメニュー「A」セクションから「Anthropic > Claude」を選択して起動します。 Claude が起動したら画面中央のテキストボックス内の左下にある「検索とツール」アイコンをクリックします。表示されたメニューに、「mcp-sios-apisl-demo」の項目と右横に数字が表示されていたら、オリジナルの MCP サーバーは正常に設定できました。 オリジナルの MCP サーバーのデモ 私たちが普段話す言葉で Claude から天気予報を教えてもらう驚きのデモンストレーションをご覧ください。 高知県の天気予報を教えてください 湘南の天気予報を教えてください 次の火曜日はゴルフなんだけど鹿沼72カントリークラブの天気を教えて まとめ オリジナルの MCP サーバーの実装手順は、いかがでしたでしょうか?MCP は生成 AI をより実用的なものにする可能性を秘めた技術だと感じています。ぜひ、皆さんの自由な発想で様々なアイデアを試してみてください。 ご覧いただきありがとうございます! この投稿はお役に立ちましたか? 役に立った 役に立たなかった 0人がこの投稿は役に立ったと言っています。 The post オリジナルのちょっと便利な MCP サーバー を作ってみた first appeared on SIOS Tech. Lab .
はじめに こんにちは、サイオステクノロジーの小沼 俊治です。 私たち API ソリューション サービスラインでパートナーとしてお取り扱いしている商材の一つである、 SaaS 型の API マネージメントプラットフォームの Kong Konnect で、自然言語にてコントロールプレーンを操作できる Kong Konnect MCP (Model Context Protocol) サーバー が GitHub に公開されました。この MCP サーバーを MCP ホストの Claude に組み込んで使ってみたので、設定方法を共有したいと思います。 Kong/mcp-konnect @ GitHub 何ができるかは、私たちが普段話す言葉で Claude から Kong Konnect を操作するデモンストレーションをご覧ください。 本資料では、既に Kong Konnect で API の管理を行っている方を対象としており、Kong Konnect MCP サーバーを MCP ホストに設定する手順に焦点を当てて解説します。従って、Kong Konnect の利用開始手順や設定、および API ゲートウェイとしての利用方法など、MCP サーバーの設定以外の内容については、本資料の範囲外とさせていただきます。 また、MCP についても既にネット上でたくさんのコンテンツが親切に説明されているので、本資料の範囲外とさせて頂きます。個人的には、KDDIアジャイル開発センター 御田さんが書かれた以下資料がお薦めです。 やさしいMCP入門 | 著者:御田 稔さま なお、ハンズオンで利用する機能は Claude の無料プランでお試し頂くことが可能ですが、便利さを感じて頂いたら是非ともアップグレードもご検討ください。 構成概要 筆者が動かした際の主な構成要素は以下の通りです。 Windows 10 Professional Claude desktop for Windows version 0.9.3 Windows PowerShell 5.1.19041.5737 Node.js v22.15.0 ハンズオンを構成する環境は以下の通りです。 MCP ホストは、Claude desktop for Windows のネイティブアプリ版を利用します 。 MCP サーバーの構成における主な考慮事項は以下の通りです 。 Kong Konnect MCP サーバーは TypeScript で実装されているため、MCP サーバーの稼働環境は Node.js を導入します 。 GitHub のリポジトリから取得した TypeScript のソースコードを JavaScript へトランスパイルして、Node.js 環境で MCP サーバーを実行します 。 実行に当たっては、Kong Konnect から Access Token などの情報を事前に収集します 。 基礎環境の構築 Claude desktop for Windows インストール MCP ホストに Web 版ではなくアプリ版の Claude desktop for Windows を利用するため、以下公式手順を参考に Windows PC へインストールします。 デスクトップ版Claudeのインストール | Anthropicヘルプセンター Node.js インストール JavaScript の実行環境が必要なため、以下手順を参考に Windows PC へ Node.js をインストールします。 初期環境構築: Node.js on Windows 事前準備 本章では Kong Konnect にログインして MCP サーバーの動作に必要な各種情報を収集します 。 Kong Admin API を利用して Kong Konnect を操作するため、Access Token を取得します。 複数のリージョンを跨いで利用できず一つのリージョンに特定する必要があるため、操作対象のリージョンを選定します。 Kong Konnect 環境から情報収集 Kong Konnect にログイン Kong Konnect のログインページにアクセスしログインします。 https://cloud.konghq.com/login Kong Konnect にログインできたら、次の章に進みます。 Personal Access Token の取得 画面右上のアカウントアイコンをクリックすると表示するアカウント向けメニューから「Personal Access Tokens」を選択します。 Personal Access Token 画面に遷移したら「+ Generate Token」ボタンをクリックしてトークンの発行処理に遷移します。 Generate a Personal Access Token ダイアログで、「Name」に発行する任意のトークン名と、「Expiration」にトークンの有効期限をリストから選択します。必要項目の入力ができたら「Generate」ボタンをクリックしてトークンを発行します。 Personal access token ダイアログで、「Copy」ボタンをクリックしてトークンをクリップボードにコピーして、このダイアログを閉じると二度とトークンの値を確認できないので、必ずコピーして控えておきます。控え終わったら「I’ve copied the token」ボタンをクリックしてダイアログを閉じます。 対象リージョンの選定 画面左下のドロップダウンにて、所属されている組織で利用可能な Kong Konnect のリージョンを切り替えることができるので、MCP を利用したいリージョンを選定します。 選定に際しては、リージョンを切り替えながら Gateway Manager を確認し、自然言語で操作したい Control Plane が存在するかを確認しながら検討していきます。 選べるリージョンの設定値は、以下 GitHub リポジトリの「Optional: The API region to use」から最新情報を確認してください。 Kong/mcp-konnect | Configuration Kong Konnect MCP サーバーの環境設定 MCP サーバーの設定 Kong Konnect MCP サーバーの GitHub リポジトリを取得して、MCP ホストの Claude と連携できるように準備します。 GitHub からリポジトリのダウンロード 「ドキュメント」フォルダ配下に、リポジトリを保存して管理する「 devlopment 」フォルダを作成します。 Path: C:\Users\{ユーザー名}\Documents\development\ {ユーザー名} はご利用中 PC のユーザー名に置き換えてください 。 ブラウザで GitHub の「Kong Konnect MCP Server」のリポジトリにアクセスします。 Kong/mcp-konnect リポジトリを ZIP ファイルで取得するため、緑色の「Code」ボタンをクリックすると表示するメニューから「Download ZIP」を選択して、名前を付けて保存するダイアログが表示されたら保存するフォルダを指定してダウンロードを開始します。 エクスプローラーでダウンロードした「 mcp-konnect-main.zip 」を右クリックして表示するメニューから「すべて展開…」を選択して ZIP ファイルを展開します。 ZIP ファイルから展開された「 mcp-konnect-main 」フォルダを、事前に準備した「 C:\Users\{ユーザー名}\Documents\development\ 」フォルダに移動するため、右クリックで表示するメニューの「切り取り」を選択して切り取ります。 なお、展開後のフォルダ構成が「 …\Donwloads\mcp-konnect-main\mcp-konnect-main\… 」のように「 mcp-konnect-main 」フォルダが二重に展開されている場合は、下段の「 mcp-konnect-main 」フォルダを移動対象にします。 事前に準備した「 C:\Users\{ユーザー名}\Documents\development\ 」フォルダをアクティブにして、右クリックのメニューから「貼り付け」を選択して「 mcp-konnect-main 」を移動します。 フォルダ名を「 mcp-konnect-main 」から末尾の main を消し「 mcp-konnect 」に変更します。 「 C:\Users\{ユーザー名}\Documents\development\mcp-konnect 」フォルダが準備できました。 Git コマンドがインストールされている PC 環境では、Windows PowerShell を使い以下コマンドでリポジトリを取得して頂くことで問題ありません。 PS C:\Users\...\development> git clone https://github.com/Kong/mcp-konnect.git 依存パッケージのインストールとトランスパイル Windows のスタートメニュー「W」セクションから「Windows PowerShell」を選択して起動します。 Window PowerShell ではコマンドラインベースで操作を行います。 リポジトリを保存したフォルダへ遷移します。 PS C:\Users\...> cd C:\Users\{ユーザー名}\Documents\development\mcp-konnect\ ソースコードが実行時に必要な依存パッケージをインストールします。 PS C:\Users\...\mcp-konnect> npm install added 109 packages, and audited 110 packages in 2s 18 packages are looking for funding run `npm fund` for details found 0 vulnerabilities 依存パッケージが格納された「 node_modules 」フォルダが新たに出来上がったことを確認します。 PS C:\Users\...\mcp-konnect> ls Mode LastWriteTime Length Name ---- ------------- ------ ---- d----- 2025/05/06 11:14 node_modules <--- 出来上がっている d----- 2025/05/06 10:57 src : ビルドして TypeScript のソースコードから Node.js で実行できる JavaScript のソースコードへトランスパイルします。 PS C:\Users\...\mcp-konnect> npm run build > kong-konnect-mcp@1.0.0 build > tsc ビルド先の「 build 」フォルダが新たに出来上がり、MCP サーバーの起動を担う「 build\index.js 」が出来上がったことを確認します。 PS C:\Users\...\mcp-konnect> ls build\ Mode LastWriteTime Length Name ---- ------------- ------ ---- d----- 2025/05/06 11:29 operations -a---- 2025/05/06 11:29 5672 api.js -a---- 2025/05/06 11:29 5675 index.js <--- 出来上がっている -a---- 2025/05/06 11:29 5617 parameters.js : Claude(MCP ホスト)の設定 Kong Konnect MCP サーバーが Claude で利用できるように設定します。 連携先 MCP サーバーの設定 Windows のスタートメニュー「A」セクションから「Anthropic > Claude」を選択して起動します。 Claude が起動したら画面左上のハンバーガーメニューから「ファイル > 設定…」を選択します。 Kong Konnect MCP サーバーが利用できるようにするために、「開発者」タブの「構成を編集」ボタンをクリックします。 エクスプローラーが立ち上がり、Claude の設定ファイルに該当する「 claude_desktop_config.json 」ファイルをダブルクリックしてエディターで開きます。 「 claude_desktop_config.json 」ファイルに Kong Konnect MCP サーバーを利用するための設定をエディターで記述して保存します。 { "mcpServers": { "kong-konnect": { "command": "node", "args": [ "C:\\Users\\{ユーザー名}\\Documents\\development\\mcp-konnect\\build\\index.js" ], "env": { "KONNECT_ACCESS_TOKEN": "kpat_1234567890abcdefghijklmnopqrstuvwxyz12345678890ab", "KONNECT_REGION": "us" } } } } 「 claude_desktop_config.json 」ファイルの編集における主な考慮事項は以下の通りです。 「mcpServers > kong-konnect > args」フィールドで主な考慮事項は以下の通りです。 node コマンドが MCP サーバーとして起動する「 index.js 」の Path を指定します。 Path にある「 {ユーザー名} 」は PC で利用しているユーザー名に置き換えます。 Path でフォルダの区切りを示す「 \ 」は二重の「 \\ 」で書く必要があります。 「mcpServers > kong-konnect > env」フィールドで主な考慮事項は以下の通りです。 MCP サーバーを起動する際に必要となる環境変数を指定します。 「 KONNECT_ACCESS_TOKEN 」環境変数には、「 Personal Access Token の取得 」章で取得した Access Token を記載します。 「 KONNECT_REGION 」環境変数には、「 対象リージョンの選定 」章で取得したリージョンの設定値を記載します。 Claude の再起動で設定反映 変更した設定ファイルの適用には、ウィンドウの「×」ボタンで終了させるだけではなく、Claude のプロセスを完全に終了させる必要があるため、Windows タスクバーの画面右側のアイコン表示領域から Claude アイコン(トゲトゲのウニの様なデザイン)を探し、右クリックで表示されるメニューから「終了」を選択してプロセスを完全に終了させます。 Windows のスタートメニュー「A」セクションから「Anthropic > Claude」を選択して起動します。 Claude が起動したら画面中央のテキストボックス内の左下にある「検索とツール」アイコンをクリックします。表示されたメニューに、「kong-konnect」の項目と右横に数字が表示されていたら、Kong Konnect MCP サーバーは正常に設定できました。 Kong Konnect MCP サーバーのデモ 私たちが普段話す言葉で Claude から Kong Konnect を操作するデモンストレーションをご覧ください。 KongでControl Plane一覧を教えてください Kongの以下Gatewayにあるサービス一覧を教えてください poc_hands-on-mcp-sios-apisl まとめ Kong Konnect MCP サーバーの組み込みとデモンストレーションはいかがでしたでしょうか。Kong に限らず、ミドルウェアベンダー各社から設定を簡略化する MCP サーバーが提供されていますので、ぜひ色々と試してみていただければと思います。 ご覧いただきありがとうございます! この投稿はお役に立ちましたか? 役に立った 役に立たなかった 0人がこの投稿は役に立ったと言っています。 The post MCP を使って 自然言語で Kong Konnect を操ってみた first appeared on SIOS Tech. Lab .
こんにちは、新卒2年目になりました、伊藤です。 昨年は、Azure Static Web AppsでGoogle認証機能を持つアプリケーションを作成する方法を紹介しました。 https://tech-lab.sios.jp/archives/43562 今回は、既存のインフラでも利用されることの多いApache HTTP Webサーバを使い、Googleアカウントで認証できるWebサーバを構築する手順をご紹介します。 設定には、ApacheのOpenID Connect (OIDC)モジュールである mod_auth_openidc を使用します。 Googleの認証プロバイダの設定 Google認証プロバイダの設定は、基本的に以前の記事( https://tech-lab.sios.jp/archives/43562 )にある『Googleの認証プロバイダを設定する』セクションの手順に従ってください。 OAuth同意画面の設定項目は次の通りです。 アプリ名 oidctest ユーザーサポートメール、デベロッパーの連絡先情報 設定者のメールアドレス アプリケーションのホームページ https://<ドメイン名> 承認済みドメイン <ドメイン名> スコープ openid, userinfo.email, userinfo.profile 認証情報の設定項目は次の通りです。 アプリケーションの種類 ウェブアプリケーション 名前 oidctest 承認済みのJavascript生成元 https://<ドメイン名> 承認済みのリダイレクト URI https://<ドメイン名>/callback OAuthクライアントを作成すると、クライアントIDとクライアントシークレットが発行されます。これらは後のApache設定で不可欠ですので、必ず安全な場所に控えておいてください。 Apache HTTP Webサーバの構築 使用環境 構築時に利用した環境は以下です。 Rocky Linux 9.2 また、OIDC認証ではHTTPS通信が前提となるため、クライアントPCからWebサーバへHTTPSでアクセスできるように設定しておいてください。 パッケージインストール 以下のパッケージをインストールします。 Apache HTTP Apache HTTP SSLモジュール Apache HTTP OpenID Connectモジュール Apache HTTPのインストール Apache HTTPをインストールし、バージョンを確認します。 $ sudo dnf -y install httpd $ httpd -v Server version: Apache/2.4.62 (Rocky Linux) Server built: Jan 10 2025 00:00:00 Apache HTTP SSLモジュールのインストール Apache HTTP SSLモジュールをインストールし、バージョンを確認します。 $ sudo dnf -y install mod_ssl $ rpm -aq | grep mod_ssl mod_ssl-2.4.62-1.el9_5.2.x86_64 Apache HTTP OpenID Connectモジュールのインストール Apache HTTP OpenID Connectモジュールをインストールし、バージョンを確認します。 $ sudo dnf -y install mod_auth_openidc $ rpm -aq | grep mod_auth_openidc mod_auth_openidc-2.4.10-1.el9.x86_64 自己署名証明書の作成 今回は検証目的のため、自己署名証明書を作成してHTTPS通信を有効にします。作成手順は以前の記事( https://tech-lab.sios.jp/archives/44751 )の『自己署名証明書の作成』セクションを参照してください。 Apache HTTPの設定 Googleアカウントの設定 Google IdP (Identity Provider) の情報を設定します。 auth_openidc_google.conf ファイルを作成します。 $ sudo vim /etc/httpd/conf/auth_openidc_google.conf OIDCProviderMetadataURL https://accounts.google.com/.well-known/openid-configuration OIDCScope "openid email profile" OIDCResponseType "code" OIDCProviderMetadataURL では、メタデータの取得先を設定します。Googleに関する設定項目を自動的に取得できます。 OIDCScope では、OAuth 同意画面で設定したスコープを設定します。 OIDCResponseType では、認証フローを設定します。Webアプリケーションでは一般的に code (Authorization Code Flow)です。 クライアント ID およびクライアントシークレットの設定 OAuthクライアントの作成時に取得したクライアントIDとクライアントシークレットを設定します。 auth_openidc_client_secret_google.conf ファイルを作成します。 機密情報を含むため、Apache実行ユーザ(通常apache)以外は読み取れないように権限を設定します。 $ sudo vim /etc/httpd/conf/auth_openidc_client_secret_google.conf OIDCClientID xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.apps.googleusercontent.com OIDCClientSecret XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX $ sudo chown apache:apache /etc/httpd/conf/auth_openidc_client_secret_google.conf $ sudo chmod 600 /etc/httpd/conf/auth_openidc_client_secret_google.conf Webサーバの設定 Apache HTTPを設定します。Apacheの基本的なセキュリティ設定(ディレクトリ一覧表示の禁止、不要なデフォルトページの無効化など)や、SSL/TLSの基本的な設定、HTTPからHTTPSへのリダイレクト設定については、以前の記事( https://tech-lab.sios.jp/archives/44751 )の『Apache HTTPの設定』セクションも参考にしてください。 OIDCの設定のうち、 OIDCRedirectURI は、認証プロバイダで設定した承認済みのリダイレクト URIと完全に一致させる必要があります。 OIDCCryptoPassphrase は、 mod_auth_openidc がセッション情報などを暗号化する際に使用するパスフレーズです。任意の十分な長さを持つ秘密の文字列を設定してください。 OIDCSSLValidateServer は、自己署名証明書を使用している場合やテスト環境ではOffにしますが、本番環境ではセキュリティ上の理由からOn(デフォルト)とし、必要に応じて証明書検証の設定を適切に行ってください。 $ sudo vim /etc/httpd/conf/httpd.conf Listen 80 Listen 443 …<省略>… ServerName <ドメイン名>:443 …<省略>… Options FollowSymLinks # Indexesを削除してディレクトリ一覧表示を禁止する …<省略>… $ sudo vim /etc/httpd/conf.d/ssl.conf #Listen 443 …<省略>… # SSL設定 SSLCertificateKeyFile /etc/pki/tls/private/server.key SSLCertificateFile /etc/pki/tls/certs/server.crt $ sudo vim /etc/httpd/conf.d/welcome.conf # # This configuration file enables the default "Welcome" page if there # is no default index page present for the root URL. To disable the # Welcome page, comment out all the lines below. # # NOTE: if this file is removed, it will be restored on upgrades. # #<LocationMatch "^/+$"> # Options -Indexes # ErrorDocument 403 /.noindex.html #</LocationMatch> …<省略>… $ sudo vim /etc/httpd/conf.d/autoindex.conf # # Directives controlling the display of server-generated directory listings. # # Required modules: mod_authz_core, mod_authz_host, # mod_autoindex, mod_alias # # To see the listing of a directory, the Options directive for the # directory must include "Indexes", and the directory must not contain # a file matching those listed in the DirectoryIndex directive. # # # IndexOptions: Controls the appearance of server-generated directory # listings. # #IndexOptions FancyIndexing HTMLTable VersionSort …<省略>… $ sudo vim /etc/httpd/conf.d/auth_openidc.conf <VirtualHost *:443> ServerName <ドメイン名> DocumentRoot /var/www/html SSLEngine on SSLProtocol all -SSLv2 -SSLv3 -TLSv1 -TLSv1.1 SSLCipherSuite EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH SSLHonorCipherOrder on SSLCertificateFile /etc/pki/tls/certs/server.crt SSLCertificateKeyFile /etc/pki/tls/private/server.key SSLCompression off SSLSessionTickets off # Google IdP (Identity Provider) の設定ファイルを読み込む Include conf/auth_openidc_google.conf # クライアント ID およびクライアントシークレットの設定ファイルを読み込む Include conf/auth_openidc_client_secret_google.conf OIDCRedirectURI https://<ドメイン名>/callback OIDCCryptoPassphrase a-random-secret-used-by-apache-oidc-and-balancer OIDCSSLValidateServer Off <Location /> # OIDCによる認証、認可を使用する AuthType openid-connect Require valid-user # 動作確認用にSSI を使えるようにする(OIDC認証したいだけであれば不要) Options +Includes DirectoryIndex index.shtml </Location> </VirtualHost> <VirtualHost *:80> # httpでアクセスした場合はhttpsに書き換える RewriteEngine On RewriteCond %{REQUEST_URI} !^/\.well\-known/acme\-challenge/ RewriteRule ^.*$ https://%{HTTP_HOST}%{REQUEST_URI} [R=301,QSA,L] </VirtualHost> 設定の確認 Apache HTTPの設定ファイルに文法的な誤りがないか確認しましょう。Syntax OKと表示されれば問題ありません。 $ sudo apachectl configtest Syntax OK サービス起動・自動起動有効化 設定を反映させるために、Apache HTTPサービスを起動(または再起動)し、OS起動時に自動で起動するように設定します。 $ sudo systemctl start httpd $ sudo systemctl enable httpd Webページの配置 OIDC認証後にユーザー情報が表示されるか確認するための簡単なテストページ( index.shtml )を、Apacheのドキュメントルート( /var/www/html/ )に配置します。このページはSSI (Server Side Includes) を使用してOIDCから渡された情報を表示します。 /var/www/html/index.shtml <DOCTYPE! html> <html lang="ja"> <head> <meta charset="UTF-8"> <title>OpenID Connect ログインしました</title> </head> <body> <h1>OpenID Connect ログインしました</h1> <p>REMOTE_USER=<!--#echo var="REMOTE_USER" --></p> <p> OIDC_CLAIM_email=<!--#echo var="OIDC_CLAIM_email" --><br/> OIDC_CLAIM_email_verified=<!--#echo var="OIDC_CLAIM_email_verified" --> <br/> </p> <p> <img src="<!--#echo var="OIDC_CLAIM_picture" -->" alt="OIDC_CLAIM_picture" /><br/> OIDC_CLAIM_name=<!--#echo var="OIDC_CLAIM_name" --><br/> OIDC_CLAIM_given_name=<!--#echo var="OIDC_CLAIM_given_name" --><br/> OIDC_CLAIM_family_name=<!--#echo var="OIDC_CLAIM_family_name" --><br/> </p> </body> </html> 動作確認 Webブラウザで https://<ドメイン名> にアクセスします。自己署名証明書を使用しているため、ブラウザにセキュリティ警告が表示されることがありますが、今回は検証目的なので「危険性を承知の上で使用」などのボタンでアクセスを続行します。 Googleのログインが求められます。 ログイン時には、同意画面が表示されます。 ログインに成功すると index.shtml にリダイレクトされます。Googleアカウントのメールアドレス、プロフィール画像、氏名などが表示されることを確認します。 まとめ 以上の手順で、Googleアカウントで認証できるApache HTTP Webサーバを構築することができました。 OIDCモジュールは他の多くの認証プロバイダにも対応しているため、応用すれば様々なサービスとの連携が可能です。機会があれば、他のプロバイダとの連携も試してみたいと思います。 参考 https://qiita.com/trueroad/items/593870ebafb675196e88 GoogleによるSSOを持つAzure Static Web Appsのアプリを作成する Apache HTTPとApache Tomcatを連携するWebサーバを構築してみた ご覧いただきありがとうございます! この投稿はお役に立ちましたか? 役に立った 役に立たなかった 0人がこの投稿は役に立ったと言っています。 The post Google認証機能を持つApache HTTP Webサーバを構築してみた first appeared on SIOS Tech. Lab .
概要 こんにちは、サイオステクノロジーの安藤 浩です。 Ethereum のテストネット: Sepolia でフルノードを構築したので フルノードから取得できるトランザクションログやイベントログから ERC-721, ERC-1155 の所有者を調べる方法をご紹介します。 前提条件 フルノードが用意されていること。 フルノードの構築方法については こちらのブログ を参照ください。 アプローチ方法 NFT の所有者を調べるには、 ERC-721 では メソッド: ownerOf があるので取得可能ですが、ノードで取得できるトランザクションログやイベントログからNFTの所有者を特定する方法を考えてみます。 ここでは、ERC-721 や ERC-1155 が主流だと思いますので、これらの所有者を調べる方法を検討してみます。 ERC-721 の場合 ERC-721 の規格に関しては以下に記載があり、event が定義されています。 ERC-721 非代替性トークン(NFT)規格 ERC-721: Non-Fungible Token Standard event Transfer(address indexed _from, address indexed _to, uint256 indexed _tokenId); _tokenId  の所有者を変更する際に event を発行することになっています。 送信元:  _from  から 送信先:  _to  に どのトークンID:  _tokenId  を送信したかをイベントログとして残しています。 また、  indexed  の付いたパラメータはそれぞれのパラメータで検索可能です。 例えば、イベントログはEtherscanだと このように なります。 イベントログで直近の送信先:  _to  が分かれば、ERC-721 の所有者だと分かります。 ロジック ロジックとしては以下のようになると思います。 現在のブロック番号から過去にさかのぼって、指定のブロック数(例: 3000ブロックの間)ごとに コントラクトアドレス に対応する  event Transfer  のイベントログを取得する。 指定のブロック数内に 検索対象の トークンID が存在しなければ次のブロックの間の  event Transfer  のイベントログを取得する。 指定のブロック数内に 検索対象の トークンID が存在したら、イベントログの To ( EOA ) を現在の所有者とする。 コードスニペット(typescript) 以下に主な箇所のスニペットを記載します。ここでは表に記載のnpm パッケージを利用しています。 npm パッケージ名 version @types/node 22.13.10 nodemon 3.1.9 ts-node 10.9.2 typescript 5.8.2 ethers 6.13.5 この関数では、引数に対象のコントラクトアドレスとトークンID、開始ブロック番号、終了ブロック番号を指定します。 event.topics[3]  がイベントログのトークンIDを示しているので、その値と引数で指定したトークンIDが一致するか比較します。一致していたら、To (  (<ethers.Log>lastEvent).topics[2]  ) に対応するアドレスをERC-721 の所有者とします。 export async function getEventTransferFilter(contractAddress: string, tokenId: bigint, fromBlock: number = 0, toBlock: number = 0): Promise<EventFilterLog | null> { try { const contract = new ethers.Contract(contractAddress, [TRANSFER_EVENT_SIGNATURE], provider); const filter = contract.filters.Transfer(null, null, null); const events = await contract.queryFilter(filter, fromBlock, toBlock); if(events.length === 0) { return null; } const matchingEvents = events.filter(event => { const eventTokenId = ethers.getBytes(event.topics[3]); const inputTokenId = ethers.getBytes(ethers.toBeHex(tokenId)); return ethers.zeroPadValue(eventTokenId, 32) === ethers.zeroPadValue(inputTokenId, 32); // TokenId と一致判定 }); if (matchingEvents.length > 0) { const lastEvent = matchingEvents[matchingEvents.length - 1]; const lastToAddress = (<ethers.Log>lastEvent).topics[2]; const lastTokenId = BigInt((<ethers.Log>lastEvent).topics[3]); return { tokenId: lastTokenId, toAddress: lastToAddress, val: BigInt(1), transactionHash: lastEvent.transactionHash, blockNumber: lastEvent.blockNumber } as EventFilterLog; } else { console.log("No matching events found."); return null; } } catch (error) { console.error("Error:", error); return null; } } ERC-1155 の場合 ERC-1155 の規格に関しては以下に記載があります。 ERC-1155 マルチトークン規格 ERC-1155: Multi Token Standard 移転に関する  event TransferSingle  ,  event TransferBatch  が定義されています。 event TransferSingle(address indexed _operator, address indexed _from, address indexed _to, uint256 _id, uint256 _value); event TransferSingle  は トークンIDが移転された際に発行されるイベントです。パラメータは以下の通りです。 パラメータ 説明 _operator トランザクションを実行したアドレス _from トークンの送信元アドレス _to トークンの送信先アドレス _id 転送されたトークンID _value 転送されたトークンの数量 event TransferBatch(address indexed _operator, address indexed _from, address indexed _to, uint256[] _ids, uint256[] _values); event TransferBatch  は、複数のトークンを一度に移動させる際に発行されるイベントです。パラメータは以下の通りです。 パラメータ 説明 _operator トランザクションを実行したアドレス _from トークンの送信元アドレス _to トークンの送信先アドレス _ids 転送されたトークンIDの配列 _values それぞれのトークンIDに対応する数量の配列 (例:  _ids[0]  のトークンが数量:  _values[0]  を転送するという意味になります) event TransferSingle ,  event TransferBatch  の2つがあるので、対象の コントラクトアドレス 、トークンID のすべてのイベントログ取得してきて、関連するアドレスの受け取った総量を計算して、送った総量を計算し、当該のアドレスの  (受け取った総量 - 送った総量 )  が0より大きいならERC-1155 を所有していることになります。 ※  event TransferBatch  に関しては  _ids  が配列で indexed ではないので絞って検索できず、全件とることになりそうだと思います。 ロジック ロジックとしては以下のようになると思います。 Contractが生成されたと思われるよりも前のブロック数から現在のブロックにさかのぼって、指定のブロック数ごとに コントラクトアドレス に対応する  event TransferSingle  と  event TransferBatch  のイベントログを取得する。 指定のブロック数内に 検索対象の トークンID が存在しなければ次のブロックの間の  event TransferSingle  と  event TransferBatch  のイベントログを取得する。 指定のブロック数内に 検索対象の トークンID が存在したら、 event TransferSingle  と  event TransferBatch  のイベントログを記録する。※  event TransferBatch  は対象でない トークンID が含まれる可能性があり、複数の EOA に送信される可能性があるため、 ids  と  values  を検索対象の トークンID のみに絞り、 ids  ごとにイベントログを記録する。 3で記録したイベントログ内でコントラクトアドレス と トークンID が一致する EOA をすべて取得する。 以下の4-1 ~ 4-3 をすべての EOA に対して行う。※ Zero Addressは除く。 4-1. from_address が EOA と一致する場合は 合計値を算出 する。 4-2. to_address が EOA と一致する場合は 合計値を算出 する。 4-3. (4-2 の値) – (4-1 の値) として、 EOA が所有する数量とする。 4-3 の所有する数量が0より大きければ、コントラクトアドレス と トークンID に対する所有者とする。 コードスニペット(typescript) ERC-1155 の場合、指定のブロック数ごとに コントラクトアドレス に対応する  event TransferSingle  と  event TransferBatch  のイベントログをすべて取得する必要があり、両方のイベントログのフォーマットを統一してDBに入れてから、イベントログを検索するようにしたいと思います。 以下にスニペットでは表に記載の npm パッケージを利用しています。 npm パッケージ名 version @types/node 22.13.10 nodemon 3.1.9 ts-node 10.9.2 typescript 5.8.2 ethers 6.13.5 mysql2 3.14.0 reflect-metadata 0.2.2 typeorm 0.3.21 import 'reflect-metadata'; import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm'; @Entity('EventTransferSingleAndBatch') export class EventTransferSingleAndBatch { @PrimaryGeneratedColumn() id!: bigint; @Column({ type: 'varchar', length: 255 }) contract_address!: string; @Column({ type: 'bigint' }) token_id!: bigint; @Column({ type: 'varchar', length: 255 }) from_address!: string; @Column({ type: 'varchar', length: 255 }) to_address!: string; @Column({ type: 'bigint' }) val!: bigint; @Column({ type: 'varchar', length: 255 }) transaction_hash!: string; @Column({ type: 'int' }) block_number!: number; @Column({ type: 'varchar', length: 255, nullable: true }) event_hash!: string; } public async processEventLogs(): Promise<void> { const startBlockNumber = this.contractBlockNumber === null ? 0 : this.contractBlockNumber; for (let i = startBlockNumber; i < this.currentBlockNumber; i += this.iter) { let toBlock = i + this.iter; let fromBlock = i; console.log(`ERC1155: ${this.contractAddress}; ${this.tokenId}. Fetching events from block ${fromBlock} to ${toBlock}...`); //NOTE: すべてのEvent ログを取得して、DB内に保存する。 let eventTransferSingleFilter = await getEventTransferSingleFilter(this.contractAddress, this.tokenId, fromBlock, toBlock); // ロジックの1,2の箇所に対応 let eventTransferBatchFilter = await getEventTransferBatchFilter(this.contractAddress, this.tokenId, fromBlock, toBlock); // ロジックの1,2の箇所に対応 eventTransferSingleFilter?.forEach(async (eventTransferSingle: EventFilterLog) => { const existEventTransferSingleAndBatch = await getEventTransferSingleAndBatch(this.contractAddress, this.tokenId, eventTransferSingle); if(existEventTransferSingleAndBatch === null) { await insertEventTransferSingleAndBatch(this.contractAddress, this.tokenId, eventTransferSingle); } }); eventTransferBatchFilter?.forEach(async (eventTransferBatch: EventTransferBatchFilterLog) => { eventTransferBatch.tokenIds.forEach(async (eventTokenId: bigint, idx: number) => { const eventTransferLog: EventFilterLog = { tokenId: eventTokenId, fromAddress: eventTransferBatch.fromAddress, toAddress: eventTransferBatch.toAddress, val: eventTransferBatch.vals[idx], transactionHash: eventTransferBatch.transactionHash, blockNumber: eventTransferBatch.blockNumber, eventHash: eventTransferBatch.eventHash } const existEventTransferSingleAndBatch = await getEventTransferSingleAndBatch(this.contractAddress, this.tokenId, eventTransferLog); if(existEventTransferSingleAndBatch === null) { await insertEventTransferSingleAndBatch(this.contractAddress, this.tokenId, eventTransferLog); } }); }); } } 説明(ロジック1~3) getEventTransferSingleFilter  ,  getEventTransferBatchFilter  の箇所がロジックの1,2に対応します。 EventTransferSingleAndBatch  のEntityを作成し、 insertEventTransferSingleAndBatch  で対象のイベントログをすべてインサートしていきます。(ロジックの3に対応) //NOTE: DB内に保存したEvent ログを元に、OwnerAddress を取得する。 public async processNftOwnerAddressForEventLog(): Promise<void> { const accountAddresses = await getAccountAddressesEventTransferSingleAndBatch(this.contractAddress, this.tokenId); accountAddresses?.forEach(async (accountAddress: string) => { //NOTE: 0x0000000000000000000000000000000000000000 が Owner というのは変なので無視して良さそう。 if(accountAddress === "0x0000000000000000000000000000000000000000") { console.log("accountAddress is zero address. skip"); return; } const accountTotalVal = await getTotalValEventTransferSingleAndBatch(this.contractAddress, this.tokenId, accountAddress); if(accountTotalVal !== null && accountTotalVal.val > 0){ const ownerRecords: Owner[] | null = await getOwnerByAccountAddress(this.contractAddress, this.tokenId, accountAddress); if(ownerRecords === null || ownerRecords.length === 0) { await insertOwnerAndNftsOwners(this.contractAddress, this.tokenId, accountAddress, accountTotalVal.val, accountTotalVal.last_tx_hash, accountTotalVal.last_block_number); } else { ownerRecords.forEach((ownerRecord: Owner) => { console.log("ownerRecord: ", ownerRecord); }) } } }); } export async function getAccountAddressesEventTransferSingleAndBatch(contractAddress: string, tokenId: bigint): Promise<string[] | null> { return connection.then(async (conn: DataSource) => { const eventTransferSingleAndBatches = await conn.getRepository(EventTransferSingleAndBatch) .find( { where: { contract_address: contractAddress, token_id: tokenId }, order: { block_number: "ASC" } } ); let accountAddresses: string[] = []; eventTransferSingleAndBatches.forEach((eventTransferSingleAndBatch: EventTransferSingleAndBatch) => { accountAddresses.push(eventTransferSingleAndBatch.from_address); accountAddresses.push(eventTransferSingleAndBatch.to_address); }); return Array.from(new Set(accountAddresses)); }) .catch((error: any) => { console.error('データベース接続エラー:', error); return null; }); } export async function getTotalValEventTransferSingleAndBatch( contractAddress: string, tokenId: bigint, accountAddress: string): Promise<Owner | null> { return await connection.then(async (conn: DataSource) => { const eventTransferSingleAndBatches = await conn.getRepository(EventTransferSingleAndBatch) .find( { where: { contract_address: contractAddress, token_id: tokenId }, order: { block_number: "asc" } } ); const eventTransferSingleAndBatchesForFromAddress = eventTransferSingleAndBatches.filter( (event: EventTransferSingleAndBatch) => { return event.from_address === accountAddress; }); const eventTransferSingleAndBatchesForToAddress = eventTransferSingleAndBatches.filter( (event: EventTransferSingleAndBatch) => { return event.to_address === accountAddress; }); let eventTransferTotalVal: bigint = BigInt(0); eventTransferSingleAndBatchesForFromAddress.forEach((eventTransferSingleAndBatch: EventTransferSingleAndBatch) => { eventTransferTotalVal -= BigInt(eventTransferSingleAndBatch.val); }); eventTransferSingleAndBatchesForToAddress.forEach((eventTransferSingleAndBatch: EventTransferSingleAndBatch) => { eventTransferTotalVal += BigInt(eventTransferSingleAndBatch.val); }); return { owner_address: accountAddress, val: eventTransferTotalVal, last_tx_hash: eventTransferSingleAndBatches[eventTransferSingleAndBatches.length - 1]?.transaction_hash, last_block_number: eventTransferSingleAndBatches[eventTransferSingleAndBatches.length - 1]?.block_number } as Owner; }) .catch((error: any) => { console.error('データベース接続エラー:', error); return null; }); } 説明(ロジック4,5) getAccountAddressesEventTransferSingleAndBatch  でコントラクトアドレスとトークンIDに対応するイベントログの to ,  from  のアドレスをすべて取得します。※  0x0000000000000000000000000000000000000000  はZero Address であり Mint や Burnで使われるアドレス なので、今回取得したいEOAではないので無視することにします。 getTotalValEventTransferSingleAndBatch  で ロジック4に対応する計算を行います。 eventTransferTotalVal  が 4-3 で求めたい総量です。 ロジック5では  eventTransferTotalVal > 0  の時に ERC-1155の所有者として、  processNftOwnerAddressForEventLog  の  insertOwnerAndNftsOwners  でレコードをインサートしています。 まとめ 規格ごとに所有者を特定する方法を検討しました。簡単にまとめると以下のようになります。 ERC-721の場合 Transfer イベントを追跡 トークンIDごとに直近のTransfer イベントの送信先を所有者とする ERC-1155の場合 TransferSingleとTransferBatchのイベントを追跡 アカウントごとに「受け取った総量」-「送った総量」で所有数を計算 所有数が0より大きい場合に所有者とする。 ERC-721 ではownerOf メソッドがあり、かつ イベントログから容易に所有者を見つけられますが、ERC-1155のような複雑な所有形態でも、イベントログから所有量を計算することで所有者を見つけることができました。 今回、Sepoliaで実施しましたが、メインネットでもロジック自体は変わらないかと思います。 ご覧いただきありがとうございます! この投稿はお役に立ちましたか? 役に立った 役に立たなかった 0人がこの投稿は役に立ったと言っています。 The post [web3] Ethereum の Sepolia上のイベントログから ERC-721, ERC-1155 の所有者の見つけ方 first appeared on SIOS Tech. Lab .
こんにちは、OSSよろず相談室の鹿島です。 今回も、オープンソースソフトウェア(OSS)に関する話題をお届けします。 これまでの記事では、OSSライセンスの基本や「コピーレフト」について解説しました。 オープンソースライセンスとは 1 ~ OSSライセンスを探してみよう オープンソースライセンスとは 2 ~コピーレフトって何だ? オープンソースライセンスとは 3 ~OSSにまつわるリアルな事例(今回の内容) 今回は 「OSSにまつわるリアルな事例」ということで、オープンソースソフトウェアにまつわる実際に起こった出来事を紹介します。 OSSライセンスの変更事例 オープンソースライセンスを、別のライセンスに変更した事例を紹介します。 Teraform まず、Teraform の事例です。 Traform は、HashiCorp社が開発する「Infrastructure as Code(IaC)」ツールで、AWSやAzure、GCPなどのクラウド環境の構築をコードで管理できます。 Teraform は OSS ですが、セキュリティ機能などを強化した商用製品も売っていました。 ところが、2023年8月に、 OSSの MPL (Mozzila Public License) から OSSではない BSL (Business Source License) に変更しました。 BSL はソースコードを公開する一方で、商用利用に制限を加えるライセンスで、競合製品の開発や提供を制限するものです。 この変更に伴い、Teraform からフォークされたオープンソースのプロジェクト「 OpenTofu 」 が立ち上げられました。 なお、2024年4月にはHashiCorp社が IBM 社に買収されましたが、Teraform の BSL ライセンスの方針は継続されています。 MongoDB MongoDB は NoSQL 型のドキュメント指向データベースとして知られ、人気のデータベースランキングでも上位に位置しています( DB-Engines Ranking 参照)。2009年に登場しました。 MongoDB は当初、 AGPL (GNU Affero General Public License ) というコピーレフト型のライセンスを採用していましたが、2018年に SSPL (Server Side Public License ) という独自ライセンスへと変更しました。 SSPL では、MongoDB をクラウドサービスとして提供する場合、そのすべてのソースコードを公開するか、MongoDBから商用ライセンスを購入する必要があります。 これは、AWSなどの大手クラウドベンダーによる“無償の OSS のただ乗り”を防ぐための対応策でした。 2018年という年は、AWSのような大手クラウドサービスが始まった時期だったためです。 AGPL も、コピーレフト型で厳しい公開の制約のあるライセンスですが、AGPL のままだと、製品に組み込むわけではなく、サービスとして丸ごと MongoDB を使用するなら“無償のOSSのただ乗り”が可能です。 そこで、このようなライセンス変更が行われたと考えられています。 OSS で訴訟に発展したケース シスコシステムズ社のルータがGPL違反!? シスコシステムズ社のルータがOSS の GPL に違反する、ということがありました。 2003年、シスコシステムズ社がリンクシス社を買収しました。 リンクシス社のルータに使われているチップに、OSS の GPL に抵触するコードが入っていました。 フリーソフトウェア財団 (FSF) がシスコシステムズ社にルータのソースコードの公開を求めて訴訟に発展しました。 参照: フリーソフトウェア財団対シスコシステムズ事件 結局、2009年にシスコシステムズ社がソースコードを公開することで和解しました。 参照: FSF(Free Software Foundation)とCisco Systems, Inc.の和解内容 著作権・ライセンスについての注意事項 本記事に記載された製品名・会社名は、それぞれの商標または登録商標です。 まとめ OSSは「無料で使える便利なソフト」と思われがちですが、実際にはライセンスに基づいたルールが厳格に存在しています。OSSのライセンス変更や違反による訴訟の事例からも分かるように、正しい理解と運用が不可欠です。 今回の一連の「OSSライセンスとは」の内容は、動画でも解説していますのでよろしければご覧ください。 YouTube: 【SOY倶楽部勉強会】オープンソースライセンスとは OSS管理の重要性と「SCANOSS」 OSS は利便性の高い技術ですが、ライセンス違反や管理不備によって訴訟リスクが発生することもあります。Terraform やMongoDB のようなライセンス変更、シスコのGPL違反のような事例は、企業にとって OSS 管理の重要性を再認識させるものです。 OSSのライセンス違反といった事態を起こさないよう、SCANOSSといったOSS管理ツールも存在し、当社では開発元である SCANOSS 社と協業を開始しました。 参照:サイオステクノロジーの SCANOSSのサポート SCANOSS は、以下のような機能を提供するOSS管理ツールです: SBOM(ソフトウェア部品表)の生成 ソースコードに含まれるOSSを自動検出 使用ライセンスの特定 スキャン対象のOSS に含まれる脆弱性検知 OSS利用が当たり前になった今こそ、「使うだけでなく、正しく使う」ための仕組みが必要です。ご興味のある方は、 当社までお問い合わせ ください。 ご覧いただきありがとうございます! この投稿はお役に立ちましたか? 役に立った 役に立たなかった 0人がこの投稿は役に立ったと言っています。 The post オープンソースライセンスとは 3 ~OSSにまつわるリアルな事例 first appeared on SIOS Tech. Lab .
こんにちは、OSSよろず相談室のSKです。 今回は、前回の OSS(オープンソースソフトウェア)とはどんなものなのかというお話の続きです。 前回の内容: オープンソースライセンスとは 1 コピーレフトとは? 今回は OSS ライセンスに関わるキーワード「 コピーレフト 」について解説します。 そもそも「ライセンス」とは? ソフトウェアを作成した著作者が、利用者に対して使用・改変・再配布を許可するための条件をまとめた文書。 これに基づいて利用者に与えられる許諾のことを「 ライセンス 」といいます。 コピーレフトの考え方 「コピーレフト」は、 フリーソフトウェア財団(FSF) の創設者 リチャード・ストールマン によって提唱された著作権の考え方です。 著作者が著作権を保持したまま、利用者に対して 複製・改変・再配布の 自由 を保証する という点が特徴です。 コピーライト vs. コピーレフト 「著作権」は英語で 「 Copyright 」 著者者の許可がないと改変、使用、再配布ができないものです。 これに対抗する形で、リチャードさんは「Left(左)」という語を使い 「 自由を保証するライセンス 」として「 Copyleft (コピーレフト)」という言葉を作りました。 「著作者の許可が無くても改変、使用、再配布ができる」ものです。 自由には制約もある? コピーレフトは「自由」を提供しますが、 その自由を他者にも提供しなければならない という「制約」もあります。 つまり、 OSSライセンスのソフトウェアを組み込んで作成したソフトウェアも自由に使用、改変、再配布できるものでなくてはならならない のです。 コピーレフトの主な制約 OSSライセンスを使って作成したソフトウェアには、次の義務があります: ソースコードを公開する必要がある 再配布も自由に許可しなければならない つまり、あなたが作ったソフトウェアがOSSのコードを含む場合、 その派生物も同様に自由である必要があるのです。 コピーレフトの分類 OSSライセンスは、「コピーレフトの強さ」によって以下のように分類されます: コピーレフト型 準コピーレフト型 非コピーレフト型 このようなライセンスの違いにより、 コードの改変・組み込み・再配布の義務範囲 が異なります。 代表的なライセンスを分類すると、表のようになります。 LinuxなどのGPLがコピーレフト型で最も厳しく、Apache Licenseなどが非コピーレフト型で緩い制約となります。 表に、 改変部分を含めたソースコードの公開 他のコードと組み合わせた場合、他のソースコードの公開 とありますが、どういうことでしょうか。 OSSライセンスを自分のソフトウェアに組み込んだ場合、コピーレフトの分類によってOSSソースコードの改変部分や組み込んだ自分のソースコードを公開するのか否かが変わります。 以下の図のようになります。 オープンソースライセンスの種類 代表的なオープンソースライセンスは以下の表のようなものがあります。 〇がコピーレフト型、△が準コピーレフト型、×が非コピーレフト型です。 次回はオープンソースにまつわるリアルな事例について、お話しします。 ご覧いただきありがとうございます! この投稿はお役に立ちましたか? 役に立った 役に立たなかった 0人がこの投稿は役に立ったと言っています。 The post オープンソースライセンスとは 2 ~コピーレフトって何だ? first appeared on SIOS Tech. Lab .