カスタム権限でカスタムMCPツールの実行を特定ユーザーだけに制限してみた

カスタム権限でカスタムMCPツールの実行を特定ユーザーだけに制限してみた

カスタムMCPサーバーのツール実行を、Salesforceのカスタム権限で制御する方法を試してみました。Apex Invocableメソッドに権限チェックを組み込むことで、特定ユーザーのみにツール実行を許可できます。
2026.06.25

前回の記事で追加したカスタムMCPサーバーのツールはApex Invocableメソッドで実行されます。
メソッド冒頭で実行ユーザーの権限を判定することで、特定ユーザーのみに実行権限を付与できるのではと思い試してみました。

カスタム権限の作成

カスタム権限を作成します。

  • 表示ラベル: 商談メモを活動として記録
  • 名前: Log_Opportunity_Meeting_Note

このカスタム権限を特定のプロファイル、もしくは権限セットで有効化することで、ツールの実行許可をコントロールします。
カスタム権限

Apexクラスの修正

前回作成したApexクラスに下記コードを追加します。

Apexクラスの冒頭にカスタム権限のAPI参照名の定数と例外を追加。

    /**
     * このツールの実行を許可するカスタム権限の API 参照名。
     * 許可するプロファイル/権限セットに、この権限を割り当てて運用する。
     */
    @TestVisible
    private static final String REQUIRED_PERMISSION = 'Log_Opportunity_Meeting_Note';

    /**
     * 権限不足など、ツールを実行できない場合に送出する例外。
     */
    global class LogOpportunityMeetingNoteException extends Exception {}

Invocableメソッドの冒頭にカスタム権限による判定を追加。

        // 実行ユーザーがカスタム権限を保持していなければ中断する。
        // throw により MCP クライアントへエラーが返り、活動は一切作成されない。
        if (!FeatureManagement.checkPermission(REQUIRED_PERMISSION)) {
            throw new LogOpportunityMeetingNoteException(
                'この操作を実行する権限がありません。' +
                '管理者にカスタム権限「商談メモを活動として記録」の付与を依頼してください。'
            );
        }

Apexクラス全体

/**
 * 商談メモ(議事録)を、指定した商談に紐づく完了済みの活動(ToDo / Task)として記録する Invocable Action。
 * カスタム MCP サーバーのツールとして公開して利用する。
 *
 * 【実行制限】カスタム権限「Log_Opportunity_Meeting_Note」を保持するユーザーのみ
 *  実行できる。許可したいプロファイルまたは権限セットにこのカスタム権限を割り当てること。
 */
global with sharing class LogOpportunityMeetingNote {

    /**
     * このツールの実行を許可するカスタム権限の API 参照名。
     * 許可するプロファイル/権限セットに、この権限を割り当てて運用する。
     */
    @TestVisible
    private static final String REQUIRED_PERMISSION = 'Log_Opportunity_Meeting_Note';

    /**
     * 権限不足など、ツールを実行できない場合に送出する例外。
     */
    global class LogOpportunityMeetingNoteException extends Exception {}

    global class Request {
        @InvocableVariable(label='商談ID' description='メモを紐付ける商談(Opportunity)のレコードID' required=true)
        global Id opportunityId;

        @InvocableVariable(label='メモ本文' description='活動として記録する議事録・商談メモの本文' required=true)
        global String notes;

        @InvocableVariable(label='件名' description='活動の件名。未指定の場合は「商談メモ」を使用')
        global String subject;

        @InvocableVariable(label='活動日' description='活動の日付。未指定の場合は本日')
        global Date activityDate;

        @InvocableVariable(label='関連連絡先ID' description='メモに紐付ける担当者(Contact)のレコードID(任意)')
        global Id contactId;
    }

    global class Result {
        @InvocableVariable(label='作成された活動ID' description='登録された活動(Task)のレコードID')
        global Id taskId;
    }

    @InvocableMethod(
        label='商談メモを活動として記録'
        description='議事録・商談メモを、指定した商談に紐づく完了済みの活動(ToDo)として記録します。'
    )
    global static List<Result> logMeetingNotes(List<Request> requests) {
        // 実行ユーザーがカスタム権限を保持していなければ中断する。
        // throw により MCP クライアントへエラーが返り、活動は一切作成されない。
        if (!FeatureManagement.checkPermission(REQUIRED_PERMISSION)) {
            throw new LogOpportunityMeetingNoteException(
                'この操作を実行する権限がありません。' +
                '管理者にカスタム権限「商談メモを活動として記録」の付与を依頼してください。'
            );
        }

        // 組織のロケール("完了"/"Completed" 等)に依存しないよう、
        // クローズ扱いの状況(Status)を動的に取得する。
        String completedStatus;
        for (TaskStatus ts : [
            SELECT MasterLabel, IsClosed, IsDefault
            FROM TaskStatus
            WHERE IsClosed = true
            WITH USER_MODE
            ORDER BY IsDefault DESC, SortOrder ASC
        ]) {
            completedStatus = ts.MasterLabel;
            break;
        }

        List<Task> tasks = new List<Task>();
        for (Request req : requests) {
            tasks.add(new Task(
                WhatId       = req.opportunityId,
                WhoId        = req.contactId,
                Subject      = String.isBlank(req.subject) ? '商談メモ' : req.subject,
                Description  = req.notes,
                ActivityDate = (req.activityDate == null) ? Date.today() : req.activityDate,
                Status       = completedStatus
            ));
        }
        insert tasks;

        List<Result> results = new List<Result>();
        for (Task t : tasks) {
            Result r = new Result();
            r.taskId = t.Id;
            results.add(r);
        }
        return results;
    }
}

動作確認

まずはカスタム権限を有効化していない状態でClaudeのチャットからツールを実行してみます。

SfOpportunityNotesを使用して、添付の議事録を006fj000009riFYAAYの商談に記録してください

実行結果です。期待通り権限エラーになりました。
権限エラー

次に実行ユーザーのプロファイルでカスタム権限を有効化します。
カスタム権限を有効化

Claudeに再実行するよう指示します。

カスタム権限「商談メモを活動として記録」を付与したのでもう一度お願いします!

実行結果です。今度は処理に成功しました。
処理成功

Salesforceからも登録が確認できました。
Salesforceでの登録確認

まとめ

カスタムMCPサーバーのツール(Apex Invocableメソッド)の冒頭で FeatureManagement.checkPermission() によるカスタム権限の判定を入れることで、特定のプロファイル・権限セットを持つユーザーだけにツールの実行を許可できました。
ポイントは以下のとおりです。

  • ツールの実体はApex Invocableメソッドなので、Apex側で権限チェックを実装すればMCPツールの実行可否をコントロールできる
  • 判定にカスタム権限を使うことで、プロファイル/権限セットのどちらにも柔軟に割り当てられ運用変更にも強い
  • 権限がない場合は例外を送出することで、MCPクライアント(Claude)側にエラーが返り、データは一切操作されない

MCPサーバー経由でSalesforceのデータを操作する場合、「誰でも実行できてしまう」状態は避けたいケースは多いはずです。今回のように既存のSalesforceの権限管理(カスタム権限)に乗せることで、アクセス制御を行うのも一つの手ではないかと思います。


Claudeならクラスメソッドにお任せください

クラスメソッドは、Anthropic社とリセラー契約を締結しています。各種製品ガイドから、業種別の活用法、フェーズごとのお悩み解決などサービス支援ページにまとめております。まずはご覧いただき、お気軽にご相談ください。

サービス詳細を見る

この記事をシェアする

関連記事