0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

MCPの活用や応用への考察 - MCPにおけるライセンス情報の管理:コンテンツ利用規約の取り扱い

Posted at

はじめに

Model Context Protocol (MCP) を活用するLLMアプリケーションでは、様々なデータソースからコンテンツを取得します。これらのコンテンツには、それぞれ利用規約やライセンス条件が存在する場合があり、適切な管理が必要です。

本記事では、MCPを使用する際のライセンス情報の取り扱い方法と、利用規約遵守をサポートする実装アプローチについて考察します。

重要な注意: 本記事は技術的な実装例を示すものであり、法的助言ではありません。実際の運用にあたっては、必ず法務専門家にご相談ください。

1. ライセンス管理の課題

1.1. LLMアプリケーションにおける課題

LLMアプリケーションが複数のデータソースを利用する際、以下のような課題があります。

課題 詳細 影響
ライセンスの多様性 データソースごとに異なる利用規約 管理の複雑化
利用目的の追跡 コンテンツがどのように使用されたか 規約違反のリスク
自動化の困難さ 人手による確認の必要性 スケーラビリティの制約
権利者への報告 利用状況の透明性確保 信頼関係の構築

1.2. 従来のアプローチの限界

手動管理の問題点:

  • データソースごとに利用規約を確認する必要がある
  • 開発者が規約を理解し、コードに反映させる必要がある
  • 規約変更時の追従が困難
  • 大規模なシステムでは実質的に管理不可能

必要なアプローチ:

  • ライセンス情報のメタデータ化
  • システムによる自動チェックのサポート
  • 利用状況の記録と監査

2. MCPにおけるライセンス情報の表現

2.1. 現在のMCP仕様

MCPの標準仕様では、リソースに対して以下のようなメタデータを付与できます。

{
  "uri": "file:///path/to/document.pdf",
  "name": "Company Annual Report 2024",
  "description": "Annual financial report",
  "mimeType": "application/pdf"
}

ライセンス情報の追加方法:

標準仕様に含まれていませんが、カスタムメタデータとしてライセンス情報を追加できます。

{
  "uri": "file:///path/to/document.pdf",
  "name": "Company Annual Report 2024",
  "mimeType": "application/pdf",
  "annotations": {
    "license": {
      "type": "CC-BY-4.0",
      "terms": "https://creativecommons.org/licenses/by/4.0/",
      "attribution": "Company Name",
      "commercialUse": true,
      "modifications": true,
      "shareAlike": false
    }
  }
}

2.2. ライセンスメタデータの設計

実用的なライセンス情報には、以下のような属性を含めることができます。

基本情報:

{
  "license": {
    "id": "license-001",
    "type": "CC-BY-NC-4.0",
    "url": "https://creativecommons.org/licenses/by-nc/4.0/",
    "summary": "Non-commercial use only with attribution"
  }
}

詳細な制約条件:

{
  "license": {
    "permissions": {
      "commercial": false,
      "modification": true,
      "distribution": true,
      "training": false
    },
    "requirements": {
      "attribution": true,
      "shareAlike": true,
      "notice": true
    },
    "limitations": {
      "liability": true,
      "warranty": true
    },
    "validUntil": "2025-12-31T23:59:59Z",
    "territorialScope": ["JP", "US", "EU"]
  }
}

3. ライセンスチェックの実装

3.1. サーバーサイドでのチェック

MCPサーバー側でライセンス情報を管理し、アクセス時にチェックする方法です。

実装例:

from dataclasses import dataclass
from typing import Optional, List
from datetime import datetime

@dataclass
class License:
    """ライセンス情報を表すクラス"""
    type: str
    commercial_use: bool
    ai_training: bool
    attribution_required: bool
    valid_until: Optional[datetime] = None
    territorial_scope: Optional[List[str]] = None
    
    def is_valid(self) -> bool:
        """ライセンスが有効期限内かチェック"""
        if self.valid_until:
            return datetime.now() < self.valid_until
        return True
    
    def allows_commercial(self) -> bool:
        """商用利用が許可されているかチェック"""
        return self.commercial_use
    
    def allows_training(self) -> bool:
        """AI学習利用が許可されているかチェック"""
        return self.ai_training

class LicenseChecker:
    """ライセンス条件をチェックするクラス"""
    
    def __init__(self):
        self.licenses = {}  # リソースIDとライセンスのマッピング
    
    def register_license(self, resource_id: str, license: License):
        """リソースのライセンス情報を登録"""
        self.licenses[resource_id] = license
    
    def check_access(
        self, 
        resource_id: str, 
        purpose: str,
        user_context: dict
    ) -> tuple[bool, str]:
        """
        アクセス可否をチェック
        
        Returns:
            (許可/拒否, 理由)
        """
        # ライセンス情報の取得
        license = self.licenses.get(resource_id)
        
        if not license:
            return True, "No license restrictions"
        
        # 有効期限チェック
        if not license.is_valid():
            return False, "License expired"
        
        # 用途チェック
        if purpose == "commercial":
            if not license.allows_commercial():
                return False, "Commercial use not allowed"
        
        if purpose == "training":
            if not license.allows_training():
                return False, "AI training use not allowed"
        
        # 地域制限チェック
        if license.territorial_scope:
            user_region = user_context.get('region')
            if user_region not in license.territorial_scope:
                return False, f"Access not allowed from {user_region}"
        
        return True, "Access granted"

# 使用例
checker = LicenseChecker()

# ライセンス登録
license = License(
    type="CC-BY-NC-4.0",
    commercial_use=False,
    ai_training=False,
    attribution_required=True,
    valid_until=datetime(2025, 12, 31)
)
checker.register_license("resource-001", license)

# アクセスチェック
allowed, reason = checker.check_access(
    resource_id="resource-001",
    purpose="commercial",
    user_context={"region": "JP"}
)

if not allowed:
    print(f"Access denied: {reason}")

3.2. クライアントサイドでのチェック

MCPクライアント(LLMアプリケーション)側でライセンス情報を確認し、適切に処理する方法です。

class MCPClientWithLicenseCheck:
    """ライセンスチェック機能付きMCPクライアント"""
    
    def __init__(self):
        self.usage_log = []
    
    async def get_resource_with_license(
        self, 
        resource_uri: str,
        intended_use: str
    ):
        """
        リソースをライセンス情報と共に取得
        """
        # MCPサーバーからリソースとメタデータを取得
        resource = await self.fetch_resource(resource_uri)
        
        # ライセンス情報の抽出
        license_info = resource.get('annotations', {}).get('license')
        
        if not license_info:
            # ライセンス情報がない場合は警告
            self.log_warning(f"No license info for {resource_uri}")
            return resource
        
        # 利用目的とライセンスの整合性チェック
        if not self._check_license_compatibility(license_info, intended_use):
            raise LicenseViolationError(
                f"Intended use '{intended_use}' not allowed by license"
            )
        
        # 利用ログに記録
        self._log_usage(resource_uri, license_info, intended_use)
        
        # 帰属表示が必要な場合は追加
        if license_info.get('requirements', {}).get('attribution'):
            resource['attribution'] = license_info.get('attribution')
        
        return resource
    
    def _check_license_compatibility(
        self, 
        license_info: dict, 
        intended_use: str
    ) -> bool:
        """ライセンスと利用目的の整合性をチェック"""
        permissions = license_info.get('permissions', {})
        
        if intended_use == 'commercial':
            return permissions.get('commercial', False)
        
        if intended_use == 'training':
            return permissions.get('training', False)
        
        return True
    
    def _log_usage(
        self, 
        resource_uri: str, 
        license_info: dict, 
        intended_use: str
    ):
        """利用状況をログに記録"""
        self.usage_log.append({
            'timestamp': datetime.now().isoformat(),
            'resource': resource_uri,
            'license': license_info.get('type'),
            'intended_use': intended_use
        })

4. 利用目的の追跡

4.1. コンテキストタグの活用

リソースが取得される際に、利用目的をタグとして付与します。

class ResourceUsageTracker:
    """リソース利用状況の追跡"""
    
    def __init__(self):
        self.usage_records = []
    
    def track_usage(
        self,
        resource_id: str,
        license_type: str,
        context: dict
    ):
        """
        リソースの利用を記録
        
        context: {
            'purpose': 'response_generation' | 'training' | 'analysis',
            'user_id': 'user-001',
            'application': 'chatbot',
            'timestamp': '2024-01-01T00:00:00Z'
        }
        """
        record = {
            'resource_id': resource_id,
            'license_type': license_type,
            'purpose': context.get('purpose'),
            'user_id': context.get('user_id'),
            'application': context.get('application'),
            'timestamp': context.get('timestamp', datetime.now().isoformat())
        }
        
        self.usage_records.append(record)
        
        # ライセンス要件に応じた処理
        self._check_usage_limits(resource_id, license_type)
    
    def _check_usage_limits(self, resource_id: str, license_type: str):
        """利用回数制限などをチェック"""
        # 同一リソースの利用回数をカウント
        usage_count = sum(
            1 for r in self.usage_records 
            if r['resource_id'] == resource_id
        )
        
        # ライセンスに応じた制限チェック
        if license_type == 'limited':
            if usage_count > 100:
                raise UsageLimitExceeded(
                    f"Usage limit exceeded for {resource_id}"
                )
    
    def generate_report(self, resource_id: str = None) -> dict:
        """利用レポートを生成"""
        records = self.usage_records
        
        if resource_id:
            records = [
                r for r in records 
                if r['resource_id'] == resource_id
            ]
        
        return {
            'total_usage': len(records),
            'by_purpose': self._group_by(records, 'purpose'),
            'by_license': self._group_by(records, 'license_type'),
            'records': records
        }
    
    def _group_by(self, records: list, key: str) -> dict:
        """レコードをキーでグループ化"""
        result = {}
        for record in records:
            value = record.get(key, 'unknown')
            result[value] = result.get(value, 0) + 1
        return result

4.2. 監査ログの生成

ライセンス関連のすべての操作を記録します。

import json
import hashlib
from datetime import datetime

class LicenseAuditLogger:
    """ライセンス監査ログ"""
    
    def __init__(self, log_file: str = "license_audit.jsonl"):
        self.log_file = log_file
    
    def log_access_attempt(
        self,
        resource_id: str,
        user_id: str,
        license_type: str,
        granted: bool,
        reason: str
    ):
        """アクセス試行を記録"""
        log_entry = {
            'event': 'access_attempt',
            'timestamp': datetime.now().isoformat(),
            'resource_id': resource_id,
            'user_id': user_id,
            'license_type': license_type,
            'granted': granted,
            'reason': reason,
            'hash': None  # 後で計算
        }
        
        # 改竄防止のためのハッシュ
        log_entry['hash'] = self._calculate_hash(log_entry)
        
        self._write_log(log_entry)
    
    def log_license_check(
        self,
        resource_id: str,
        checks_performed: list,
        result: dict
    ):
        """ライセンスチェックの詳細を記録"""
        log_entry = {
            'event': 'license_check',
            'timestamp': datetime.now().isoformat(),
            'resource_id': resource_id,
            'checks': checks_performed,
            'result': result,
            'hash': None
        }
        
        log_entry['hash'] = self._calculate_hash(log_entry)
        self._write_log(log_entry)
    
    def _calculate_hash(self, log_entry: dict) -> str:
        """ログエントリのハッシュを計算"""
        # hashフィールドを除外してハッシュ計算
        entry_copy = {k: v for k, v in log_entry.items() if k != 'hash'}
        entry_str = json.dumps(entry_copy, sort_keys=True)
        return hashlib.sha256(entry_str.encode()).hexdigest()
    
    def _write_log(self, log_entry: dict):
        """ログファイルに書き込み"""
        with open(self.log_file, 'a') as f:
            f.write(json.dumps(log_entry) + '\n')
    
    def verify_log_integrity(self) -> bool:
        """ログの整合性を検証"""
        with open(self.log_file, 'r') as f:
            for line in f:
                entry = json.loads(line)
                stored_hash = entry.get('hash')
                calculated_hash = self._calculate_hash(entry)
                
                if stored_hash != calculated_hash:
                    print(f"Log integrity violation detected!")
                    return False
        
        return True

5. 実装上の考慮事項

5.1. パフォーマンスへの影響

ライセンスチェックは、リソース取得のたびに実行されるため、パフォーマンスへの影響を考慮する必要があります。

最適化手法:

from functools import lru_cache
from datetime import datetime, timedelta

class CachedLicenseChecker:
    """キャッシュ機能付きライセンスチェッカー"""
    
    def __init__(self, cache_ttl_seconds: int = 300):
        self.cache_ttl = timedelta(seconds=cache_ttl_seconds)
        self.cache = {}
    
    def check_license(self, resource_id: str, context: dict) -> tuple[bool, str]:
        """キャッシュを使用したライセンスチェック"""
        cache_key = self._make_cache_key(resource_id, context)
        
        # キャッシュ確認
        if cache_key in self.cache:
            cached_result, cached_time = self.cache[cache_key]
            if datetime.now() - cached_time < self.cache_ttl:
                return cached_result
        
        # 実際のチェック
        result = self._perform_license_check(resource_id, context)
        
        # キャッシュに保存
        self.cache[cache_key] = (result, datetime.now())
        
        return result
    
    def _make_cache_key(self, resource_id: str, context: dict) -> str:
        """キャッシュキーを生成"""
        relevant_context = {
            'purpose': context.get('purpose'),
            'user_id': context.get('user_id')
        }
        return f"{resource_id}:{json.dumps(relevant_context, sort_keys=True)}"
    
    def _perform_license_check(
        self, 
        resource_id: str, 
        context: dict
    ) -> tuple[bool, str]:
        """実際のライセンスチェック処理"""
        # 実装は省略
        pass

5.2. エラーハンドリング

ライセンス違反を検出した際の適切な処理が重要です。

class LicenseViolationError(Exception):
    """ライセンス違反エラー"""
    pass

class LicenseExpiredError(LicenseViolationError):
    """ライセンス期限切れエラー"""
    pass

def handle_resource_access(resource_id: str, purpose: str):
    """リソースアクセス処理(エラーハンドリング付き)"""
    try:
        # ライセンスチェック
        checker = LicenseChecker()
        allowed, reason = checker.check_access(resource_id, purpose, {})
        
        if not allowed:
            if "expired" in reason.lower():
                raise LicenseExpiredError(reason)
            else:
                raise LicenseViolationError(reason)
        
        # リソース取得
        resource = fetch_resource(resource_id)
        return resource
        
    except LicenseExpiredError as e:
        # 期限切れの場合は更新を促す
        logger.warning(f"License expired for {resource_id}: {e}")
        notify_license_renewal_needed(resource_id)
        return None
        
    except LicenseViolationError as e:
        # その他の違反は記録して拒否
        logger.error(f"License violation for {resource_id}: {e}")
        log_violation_attempt(resource_id, purpose)
        return None

5.3. ユーザーへの通知

ライセンス制約について、ユーザーに適切に通知する必要があります。

def get_resource_with_user_notification(
    resource_id: str,
    purpose: str,
    user_interface
):
    """ユーザー通知付きリソース取得"""
    checker = LicenseChecker()
    allowed, reason = checker.check_access(resource_id, purpose, {})
    
    if not allowed:
        # ユーザーに理由を説明
        user_interface.show_warning(
            title="License Restriction",
            message=f"This content cannot be used for {purpose}. Reason: {reason}",
            alternatives=get_alternative_resources(resource_id)
        )
        return None
    
    # ライセンス情報を取得
    license_info = get_license_info(resource_id)
    
    # 帰属表示が必要な場合
    if license_info.get('attribution_required'):
        attribution = license_info.get('attribution')
        user_interface.add_citation(attribution)
    
    return fetch_resource(resource_id)

6. 権利者への報告

6.1. 利用レポートの生成

定期的に権利者へ利用状況を報告します。

class UsageReporter:
    """利用状況レポート生成"""
    
    def __init__(self, tracker: ResourceUsageTracker):
        self.tracker = tracker
    
    def generate_monthly_report(
        self,
        resource_id: str,
        month: str
    ) -> dict:
        """月次レポートを生成"""
        records = self._get_records_for_month(resource_id, month)
        
        return {
            'resource_id': resource_id,
            'period': month,
            'total_accesses': len(records),
            'by_purpose': self._analyze_by_purpose(records),
            'by_application': self._analyze_by_application(records),
            'unique_users': len(set(r['user_id'] for r in records)),
            'compliance_status': self._check_compliance(records)
        }
    
    def send_report_to_rightholder(
        self,
        rightholder_email: str,
        report: dict
    ):
        """権利者にレポートを送信"""
        # メール送信やAPI経由での通知
        # 実装は省略
        pass

6.2. リアルタイム通知

重要なイベントは即座に通知します。

class LicenseEventNotifier:
    """ライセンスイベント通知"""
    
    def notify_usage_threshold(
        self,
        resource_id: str,
        current_usage: int,
        threshold: int
    ):
        """利用回数が閾値に達した場合に通知"""
        if current_usage >= threshold:
            message = f"Resource {resource_id} has reached {current_usage} uses (threshold: {threshold})"
            self._send_notification(message)
    
    def notify_violation_attempt(
        self,
        resource_id: str,
        violation_type: str,
        details: dict
    ):
        """ライセンス違反の試みを通知"""
        message = f"License violation attempt detected for {resource_id}: {violation_type}"
        self._send_alert(message, details)
    
    def _send_notification(self, message: str):
        """通知を送信(通常の優先度)"""
        # 実装は省略
        pass
    
    def _send_alert(self, message: str, details: dict):
        """アラートを送信(高い優先度)"""
        # 実装は省略
        pass

7. 標準ライセンスへの対応

7.1. Creative Commonsライセンス

class CreativeCommonsLicense:
    """Creative Commonsライセンスのサポート"""
    
    LICENSE_TYPES = {
        'CC0': {
            'commercial': True,
            'modification': True,
            'distribution': True,
            'attribution': False
        },
        'CC-BY': {
            'commercial': True,
            'modification': True,
            'distribution': True,
            'attribution': True
        },
        'CC-BY-SA': {
            'commercial': True,
            'modification': True,
            'distribution': True,
            'attribution': True,
            'shareAlike': True
        },
        'CC-BY-NC': {
            'commercial': False,
            'modification': True,
            'distribution': True,
            'attribution': True
        },
        'CC-BY-NC-SA': {
            'commercial': False,
            'modification': True,
            'distribution': True,
            'attribution': True,
            'shareAlike': True
        },
        'CC-BY-ND': {
            'commercial': True,
            'modification': False,
            'distribution': True,
            'attribution': True
        },
        'CC-BY-NC-ND': {
            'commercial': False,
            'modification': False,
            'distribution': True,
            'attribution': True
        }
    }
    
    @classmethod
    def get_permissions(cls, license_type: str) -> dict:
        """ライセンスタイプから許可内容を取得"""
        return cls.LICENSE_TYPES.get(license_type, {})
    
    @classmethod
    def check_compatibility(
        cls,
        license_type: str,
        intended_use: dict
    ) -> bool:
        """利用目的との互換性をチェック"""
        permissions = cls.get_permissions(license_type)
        
        if intended_use.get('commercial') and not permissions.get('commercial'):
            return False
        
        if intended_use.get('modification') and not permissions.get('modification'):
            return False
        
        return True

まとめ

MCPを使用する際のライセンス管理には、以下のようなアプローチが有効です。

実践的な実装ステップ:

  1. メタデータの設計: リソースにライセンス情報を付与
  2. チェック機構の実装: サーバーまたはクライアントでのチェック
  3. ログと追跡: 利用状況の記録
  4. 報告とコンプライアンス: 権利者への報告

重要な原則:

  • 透明性: すべての利用を記録し、追跡可能にする
  • 自動化: 可能な限り手動チェックを減らす
  • 柔軟性: 様々なライセンス形態に対応できる設計
  • ユーザー体験: ライセンス制約を明確に伝える

制限と課題:

  • 完全な自動化は困難(人間による判断が必要な場合もある)
  • ライセンス解釈の複雑さ(法的専門知識が必要)
  • パフォーマンスとセキュリティのトレードオフ

法的な注意:

本記事は技術的な実装例を示すものであり、法的助言ではありません。ライセンス管理の実装にあたっては、必ず法務専門家に相談し、適用される法律や規制を遵守してください。


参考情報

免責事項: 本記事の内容は情報提供のみを目的としており、法的助言を構成するものではありません。実際の運用にあたっては、必ず法律専門家にご相談ください。


補足:実践的な統合例

8. 完全な実装例

以下は、MCPサーバーにライセンスチェック機能を統合した完全な例です。

import json
from datetime import datetime
from typing import Optional, Dict, List, Any
from dataclasses import dataclass, asdict

@dataclass
class LicenseInfo:
    """ライセンス情報"""
    license_id: str
    license_type: str
    commercial_use: bool
    ai_training: bool
    modification: bool
    attribution_required: bool
    attribution_text: Optional[str] = None
    valid_until: Optional[str] = None
    territorial_scope: Optional[List[str]] = None
    usage_limit: Optional[int] = None
    url: Optional[str] = None

class LicenseManager:
    """
    ライセンス管理の統合クラス
    チェック、追跡、ログ、レポート機能を提供
    """
    
    def __init__(self, config_file: str = "licenses.json"):
        self.config_file = config_file
        self.licenses: Dict[str, LicenseInfo] = {}
        self.usage_log: List[Dict] = []
        self.violation_log: List[Dict] = []
        self._load_licenses()
    
    def _load_licenses(self):
        """設定ファイルからライセンス情報を読み込み"""
        try:
            with open(self.config_file, 'r') as f:
                data = json.load(f)
                for resource_id, license_data in data.items():
                    self.licenses[resource_id] = LicenseInfo(**license_data)
        except FileNotFoundError:
            print(f"License config file not found: {self.config_file}")
    
    def register_resource(self, resource_id: str, license_info: LicenseInfo):
        """新しいリソースのライセンス情報を登録"""
        self.licenses[resource_id] = license_info
        self._save_licenses()
    
    def _save_licenses(self):
        """ライセンス情報を保存"""
        data = {
            resource_id: asdict(license_info) 
            for resource_id, license_info in self.licenses.items()
        }
        with open(self.config_file, 'w') as f:
            json.dump(data, f, indent=2)
    
    def check_and_log_access(
        self,
        resource_id: str,
        user_id: str,
        purpose: str,
        context: Dict[str, Any]
    ) -> tuple[bool, str, Optional[Dict]]:
        """
        アクセスチェックとログ記録を一度に実行
        
        Returns:
            (許可/拒否, 理由, ライセンス情報)
        """
        # ライセンス情報取得
        license_info = self.licenses.get(resource_id)
        
        if not license_info:
            # ライセンス情報がない場合は警告付きで許可
            self._log_access(resource_id, user_id, purpose, True, 
                           "No license restrictions", context)
            return True, "No license restrictions", None
        
        # 各種チェック
        checks = []
        
        # 1. 有効期限チェック
        if license_info.valid_until:
            expiry = datetime.fromisoformat(license_info.valid_until)
            if datetime.now() > expiry:
                reason = f"License expired on {license_info.valid_until}"
                self._log_violation(resource_id, user_id, purpose, reason, context)
                return False, reason, asdict(license_info)
            checks.append("expiry_check_passed")
        
        # 2. 用途チェック
        if purpose == "commercial" and not license_info.commercial_use:
            reason = "Commercial use not permitted by license"
            self._log_violation(resource_id, user_id, purpose, reason, context)
            return False, reason, asdict(license_info)
        checks.append("purpose_check_passed")
        
        if purpose == "training" and not license_info.ai_training:
            reason = "AI training use not permitted by license"
            self._log_violation(resource_id, user_id, purpose, reason, context)
            return False, reason, asdict(license_info)
        
        # 3. 地域制限チェック
        if license_info.territorial_scope:
            user_region = context.get('region', 'unknown')
            if user_region not in license_info.territorial_scope:
                reason = f"Access not allowed from region: {user_region}"
                self._log_violation(resource_id, user_id, purpose, reason, context)
                return False, reason, asdict(license_info)
            checks.append("region_check_passed")
        
        # 4. 利用回数制限チェック
        if license_info.usage_limit:
            current_usage = self._count_usage(resource_id)
            if current_usage >= license_info.usage_limit:
                reason = f"Usage limit reached: {current_usage}/{license_info.usage_limit}"
                self._log_violation(resource_id, user_id, purpose, reason, context)
                return False, reason, asdict(license_info)
            checks.append("usage_limit_check_passed")
        
        # すべてのチェックを通過
        self._log_access(resource_id, user_id, purpose, True, 
                        f"All checks passed: {', '.join(checks)}", context)
        
        return True, "Access granted", asdict(license_info)
    
    def _log_access(
        self,
        resource_id: str,
        user_id: str,
        purpose: str,
        granted: bool,
        reason: str,
        context: Dict
    ):
        """アクセスログを記録"""
        log_entry = {
            'timestamp': datetime.now().isoformat(),
            'event_type': 'access',
            'resource_id': resource_id,
            'user_id': user_id,
            'purpose': purpose,
            'granted': granted,
            'reason': reason,
            'context': context
        }
        self.usage_log.append(log_entry)
    
    def _log_violation(
        self,
        resource_id: str,
        user_id: str,
        purpose: str,
        reason: str,
        context: Dict
    ):
        """ライセンス違反の試みを記録"""
        violation_entry = {
            'timestamp': datetime.now().isoformat(),
            'resource_id': resource_id,
            'user_id': user_id,
            'purpose': purpose,
            'reason': reason,
            'context': context
        }
        self.violation_log.append(violation_entry)
        
        # 重大な違反は即座に通知
        self._send_violation_alert(violation_entry)
    
    def _count_usage(self, resource_id: str) -> int:
        """特定リソースの利用回数をカウント"""
        return sum(
            1 for log in self.usage_log 
            if log['resource_id'] == resource_id and log['granted']
        )
    
    def _send_violation_alert(self, violation: Dict):
        """違反アラートを送信(実装例)"""
        print(f"⚠️  LICENSE VIOLATION ALERT:")
        print(f"   Resource: {violation['resource_id']}")
        print(f"   User: {violation['user_id']}")
        print(f"   Reason: {violation['reason']}")
        print(f"   Time: {violation['timestamp']}")
    
    def generate_usage_report(
        self,
        resource_id: Optional[str] = None,
        start_date: Optional[str] = None,
        end_date: Optional[str] = None
    ) -> Dict:
        """利用レポートを生成"""
        logs = self.usage_log
        
        # フィルタリング
        if resource_id:
            logs = [l for l in logs if l['resource_id'] == resource_id]
        
        if start_date:
            logs = [l for l in logs if l['timestamp'] >= start_date]
        
        if end_date:
            logs = [l for l in logs if l['timestamp'] <= end_date]
        
        # 統計計算
        total_accesses = len(logs)
        granted = sum(1 for l in logs if l['granted'])
        denied = total_accesses - granted
        
        # 用途別集計
        by_purpose = {}
        for log in logs:
            purpose = log.get('purpose', 'unknown')
            by_purpose[purpose] = by_purpose.get(purpose, 0) + 1
        
        # ユーザー別集計
        unique_users = len(set(l['user_id'] for l in logs))
        
        return {
            'summary': {
                'total_accesses': total_accesses,
                'granted': granted,
                'denied': denied,
                'unique_users': unique_users
            },
            'by_purpose': by_purpose,
            'period': {
                'start': start_date or 'beginning',
                'end': end_date or 'now'
            },
            'violations': len(self.violation_log)
        }
    
    def export_logs(self, filename: str):
        """ログをファイルにエクスポート"""
        export_data = {
            'exported_at': datetime.now().isoformat(),
            'usage_logs': self.usage_log,
            'violation_logs': self.violation_log
        }
        
        with open(filename, 'w') as f:
            json.dump(export_data, f, indent=2)
        
        print(f"Logs exported to {filename}")

# 使用例
def example_usage():
    """実際の使用例"""
    
    # ライセンスマネージャーの初期化
    manager = LicenseManager()
    
    # リソースの登録
    license1 = LicenseInfo(
        license_id="lic-001",
        license_type="CC-BY-NC-4.0",
        commercial_use=False,
        ai_training=False,
        modification=True,
        attribution_required=True,
        attribution_text="© 2024 Example Corp",
        valid_until="2025-12-31T23:59:59",
        territorial_scope=["JP", "US"],
        usage_limit=1000,
        url="https://creativecommons.org/licenses/by-nc/4.0/"
    )
    
    manager.register_resource("document-001", license1)
    
    # アクセスチェック(商用利用を試みる)
    allowed, reason, license_info = manager.check_and_log_access(
        resource_id="document-001",
        user_id="user-123",
        purpose="commercial",
        context={
            'region': 'JP',
            'application': 'chatbot',
            'ip_address': '192.168.1.1'
        }
    )
    
    if not allowed:
        print(f"❌ Access denied: {reason}")
        if license_info and license_info.get('attribution_text'):
            print(f"   License: {license_info['license_type']}")
            print(f"   URL: {license_info.get('url', 'N/A')}")
    else:
        print(f"✅ Access granted: {reason}")
        if license_info and license_info.get('attribution_required'):
            print(f"   Attribution required: {license_info['attribution_text']}")
    
    # 非商用利用を試みる(成功するはず)
    allowed2, reason2, license_info2 = manager.check_and_log_access(
        resource_id="document-001",
        user_id="user-123",
        purpose="research",
        context={
            'region': 'JP',
            'application': 'research_tool'
        }
    )
    
    print(f"\n{'' if allowed2 else ''} Second access: {reason2}")
    
    # レポート生成
    report = manager.generate_usage_report(resource_id="document-001")
    print(f"\n📊 Usage Report:")
    print(json.dumps(report, indent=2))
    
    # ログのエクスポート
    manager.export_logs("license_logs.json")

if __name__ == "__main__":
    example_usage()

9. MCPサーバーへの統合

実際のMCPサーバーに統合する例:

from mcp.server import Server
from mcp.server.stdio import stdio_server
from mcp.types import Resource, Tool

class LicensedMCPServer:
    """ライセンスチェック機能を持つMCPサーバー"""
    
    def __init__(self):
        self.server = Server("licensed-content-server")
        self.license_manager = LicenseManager()
        self._setup_handlers()
    
    def _setup_handlers(self):
        """ハンドラーの設定"""
        
        @self.server.list_resources()
        async def list_resources() -> list[Resource]:
            """利用可能なリソースをリスト"""
            # ライセンス情報を含むリソースリストを返す
            resources = []
            for resource_id, license_info in self.license_manager.licenses.items():
                resources.append(Resource(
                    uri=f"licensed://{resource_id}",
                    name=resource_id,
                    mimeType="text/plain",
                    description=f"License: {license_info.license_type}",
                    annotations={
                        "license": asdict(license_info)
                    }
                ))
            return resources
        
        @self.server.read_resource()
        async def read_resource(uri: str) -> str:
            """リソースの読み取り(ライセンスチェック付き)"""
            # URIからリソースIDを抽出
            resource_id = uri.replace("licensed://", "")
            
            # コンテキスト情報(実際のアプリケーションから取得)
            context = {
                'region': 'JP',
                'application': 'mcp-client',
                'timestamp': datetime.now().isoformat()
            }
            
            # ライセンスチェック
            allowed, reason, license_info = self.license_manager.check_and_log_access(
                resource_id=resource_id,
                user_id="default-user",  # 実際は認証から取得
                purpose="read",
                context=context
            )
            
            if not allowed:
                raise PermissionError(f"License check failed: {reason}")
            
            # リソースの内容を取得
            content = self._fetch_resource_content(resource_id)
            
            # 帰属表示が必要な場合は追加
            if license_info and license_info.get('attribution_required'):
                attribution = license_info.get('attribution_text', '')
                content = f"{content}\n\n---\nSource: {attribution}"
            
            return content
        
        @self.server.call_tool()
        async def call_tool(name: str, arguments: dict) -> list[Any]:
            """ツール実行(用途に応じたライセンスチェック)"""
            
            if name == "get_usage_report":
                # 利用レポートの取得
                resource_id = arguments.get("resource_id")
                report = self.license_manager.generate_usage_report(resource_id)
                return [{"type": "text", "text": json.dumps(report, indent=2)}]
            
            elif name == "check_license":
                # ライセンス情報の確認
                resource_id = arguments.get("resource_id")
                license_info = self.license_manager.licenses.get(resource_id)
                if license_info:
                    return [{"type": "text", "text": json.dumps(asdict(license_info), indent=2)}]
                else:
                    return [{"type": "text", "text": "No license information found"}]
            
            else:
                raise ValueError(f"Unknown tool: {name}")
    
    def _fetch_resource_content(self, resource_id: str) -> str:
        """リソースの実際のコンテンツを取得(実装例)"""
        # 実際のシステムでは、データベースやファイルシステムから取得
        return f"Content of {resource_id}"
    
    async def run(self):
        """サーバーの起動"""
        async with stdio_server() as (read_stream, write_stream):
            await self.server.run(
                read_stream,
                write_stream,
                self.server.create_initialization_options()
            )

# サーバーの起動
if __name__ == "__main__":
    import asyncio
    server = LicensedMCPServer()
    asyncio.run(server.run())

10. クライアント側での活用

MCPクライアント側でライセンス情報を適切に扱う例:

class LicenseAwareClient:
    """ライセンス情報を考慮するMCPクライアント"""
    
    def __init__(self, mcp_client):
        self.mcp_client = mcp_client
        self.license_cache = {}
    
    async def get_resource_safely(
        self,
        resource_uri: str,
        intended_use: str
    ) -> tuple[Optional[str], Optional[Dict]]:
        """
        ライセンスを確認しながらリソースを取得
        
        Returns:
            (コンテンツ, ライセンス情報)
        """
        try:
            # リソース一覧を取得してライセンス情報を確認
            resources = await self.mcp_client.list_resources()
            
            target_resource = None
            for resource in resources:
                if resource.uri == resource_uri:
                    target_resource = resource
                    break
            
            if not target_resource:
                print(f"Resource not found: {resource_uri}")
                return None, None
            
            # ライセンス情報の取得
            license_info = target_resource.annotations.get('license', {})
            
            # 意図した用途がライセンスで許可されているか確認
            if not self._check_license_allows(license_info, intended_use):
                print(f"⚠️  Warning: Intended use '{intended_use}' may not be allowed by license")
                print(f"   License type: {license_info.get('license_type', 'Unknown')}")
                
                # ユーザーに確認を求める
                if not self._get_user_confirmation(license_info):
                    return None, license_info
            
            # リソースを取得
            content = await self.mcp_client.read_resource(resource_uri)
            
            # ライセンス情報をキャッシュ
            self.license_cache[resource_uri] = license_info
            
            return content, license_info
            
        except PermissionError as e:
            print(f"❌ Access denied: {e}")
            return None, None
    
    def _check_license_allows(self, license_info: Dict, intended_use: str) -> bool:
        """ライセンスが意図した用途を許可しているかチェック"""
        if intended_use == "commercial":
            return license_info.get('commercial_use', False)
        elif intended_use == "training":
            return license_info.get('ai_training', False)
        elif intended_use == "modification":
            return license_info.get('modification', False)
        else:
            return True  # その他の用途は一般的に許可
    
    def _get_user_confirmation(self, license_info: Dict) -> bool:
        """ユーザーに確認を求める(簡易実装)"""
        print("\nLicense Information:")
        print(f"  Type: {license_info.get('license_type', 'Unknown')}")
        print(f"  Commercial use: {license_info.get('commercial_use', False)}")
        print(f"  AI training: {license_info.get('ai_training', False)}")
        print(f"  Attribution required: {license_info.get('attribution_required', False)}")
        
        if license_info.get('url'):
            print(f"  Full license: {license_info['url']}")
        
        response = input("\nDo you want to proceed? (yes/no): ")
        return response.lower() in ['yes', 'y']
    
    def format_citation(self, resource_uri: str) -> str:
        """リソースの引用情報をフォーマット"""
        license_info = self.license_cache.get(resource_uri, {})
        
        if not license_info.get('attribution_required'):
            return ""
        
        attribution = license_info.get('attribution_text', 'Source unknown')
        license_type = license_info.get('license_type', '')
        url = license_info.get('url', '')
        
        citation = f"\n---\nSource: {attribution}"
        if license_type:
            citation += f"\nLicense: {license_type}"
        if url:
            citation += f"\nLicense URL: {url}"
        
        return citation

# 使用例
async def example_client_usage():
    """クライアントの使用例"""
    from mcp.client import ClientSession, StdioServerParameters
    from mcp import stdio_client
    
    # MCPサーバーに接続
    server_params = StdioServerParameters(
        command="python",
        args=["licensed_mcp_server.py"]
    )
    
    async with stdio_client(server_params) as (read, write):
        async with ClientSession(read, write) as session:
            await session.initialize()
            
            # ライセンス対応クライアントの作成
            client = LicenseAwareClient(session)
            
            # リソースを安全に取得
            content, license_info = await client.get_resource_safely(
                resource_uri="licensed://document-001",
                intended_use="commercial"
            )
            
            if content:
                print(f"\n✅ Content retrieved successfully")
                print(content)
                
                # 引用情報を追加
                citation = client.format_citation("licensed://document-001")
                if citation:
                    print(citation)
            else:
                print(f"\n❌ Could not retrieve content")

これらの実装例により、MCPを使用する際に実践的なライセンス管理を行うことができます。


注意: MCPはAnthropicが開発した比較的新しいプロトコルです。最新の情報については、公式ドキュメントを参照してください。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?