みなさん、こんにちは!
TDX 2026 で発表された HXL(Headless Experience Layer) ですが、2026年8月後半よりベータ公開となりました。Salesforce のデータを Slackbot、ChatGPT や Claude といった外部の AI エージェントアプリに、リッチなカスタム UI(ウィジェット)として表示できます。しかも表示するのは静的なサンプルではなく、組織の実データです。
本記事では、その HXL ウィジェットを MCP(Model Context Protocol)サーバー経由で公開し、AI エージェントアプリに「取引先の商談サマリ」を表示するまでをの流れを解説します。完成すると、AI エージェントアプリで「株式会社アストロエンタープライズの商談状況サマリを教えて」などと話しかけるだけで、次のようなカードが返ってくるようになります。
HXL の仕様や API の詳細は、公式の Headless Experience Layer Developer Guide (Beta) に詳しくまとまっています。ただし内容は英語で、サンプルもオブジェクトのデータにはアクセスしないものでした。そこで本記事では、実際に Salesforce 組織のデータを扱いながら、日本語で一連の流れを解説することを目的に作成しました。公式ガイドとあわせて読むことで、より理解が深まるはずです。
対象読者は Salesforce 開発の経験がある方です。Apex、メタデータ、VS Code + Salesforce&Agentforce 関連の拡張機能、sf コマンドの基礎は前提とし、HXL・MCP まわりの新しい概念を重点的に解説します。
※注意
- この記事は 2026 年 8 月時点の情報で作成しています。HXL / MCP まわりは進化が速いため、最新の仕様は公式ドキュメントもあわせてご確認ください。
- 本番組織ではなく Sandbox / Developer Editio/ Scratch Org での検証をおすすめします。
- 本記事の内容を試すには、組織のインスタンスが Summer ’26 Patch 14.4 以降である必要があります。パッチバージョンは Salesforce Status で自組織のインスタンスを検索すると確認できます。
- 本手順は Developer Edition での動作を確認しています。
全体の流れ
作るファイルはやや多いですが、役割ごとに整理すると次の 7 ステップになります。
- Apex クラスで取引先の商談サマリーを取得する(データ取得ロジック)
- LightningType で戻り値の型とレンダラー(描画マッピング)を定義する
- UiWidgetBundle でウィジェットの見た目(UI)を作る
- GenAiFunction(Agent Action) で Apex を API カタログに登録する
- McpServerDefinition で MCP サーバーとして公開する
- デプロイする
- MCP サーバーを有効化し、AI エージェントアプリから接続して動作確認する
登場人物を図にすると、データの流れは次のようになります。
ポイントは、「データを取得する経路(tools)」と「見た目を定義する経路(resources)」が別々で、それを McpServerDefinition が束ねているところです。ここを押さえると、以降のファイル構成がすっきり理解できます。
事前準備
- Salesforce 組織(検証には Developer Edition または Sandbox を推奨)
- Salesforce CLI(
sf)& VS Code + Salesforce & Agentforce の拡張機能
本記事では API バージョン 67.0 を前提にしています。
HXL を有効化する
まず、組織で HXL の機能を有効化しておきます。設定 のクイック検索で「ヘッドレスエクスペリエンスレイヤー」を検索し、設定ページを開いて有効化スイッチをオンにします。
※ この設定が表示されない場合は、組織のインスタンスが Summer ’26 Patch 14.4 以降になっているかを確認してください(パッチバージョンの確認方法は冒頭の注意を参照)。
📦 本記事で作成するコード一式は、GitHub リポジトリ hinabasfdc/hxl-mcp-widget-sample に公開しています。手を動かしながら読む場合は、あわせて参照してください。
ステップ 1:Apex で商談サマリーを取得する
まずはデータ取得の心臓部となる Apex クラスです。取引先名を受け取り、次の 3 つを返します。
- 商談の総数
- フェーズ(
StageName)別の商談件数 - 未クローズ(
IsClosed = false)商談の合計金額
MCP のツールとして呼び出せるように、@InvocableMethod / @InvocableVariable で入出力を定義するのがポイントです。クラス全体は次のとおりです。
1global without sharing class AccountOpportunitySummary {
2
3 @InvocableMethod(label='取引先の商談サマリーを取得' description='取引先名から、商談件数・フェーズ別件数・未クローズ商談の合計金額を取得します。')
4 global static List<SummaryResponse> getAccountOpportunitySummary(List<SummaryRequest> requests) {
5 List<SummaryResponse> responses = new List<SummaryResponse>();
6
7 for (SummaryRequest req : requests) {
8 SummaryResponse res = new SummaryResponse();
9 res.summary = buildSummary(req.accountName);
10 responses.add(res);
11 }
12
13 return responses;
14 }
15
16 private static Summary buildSummary(String accountName) {
17 Summary summary = new Summary();
18 summary.accountName = accountName;
19 summary.accountInitials = toInitials(accountName);
20 summary.totalOpportunityCount = 0;
21 summary.stageSummaryText = '';
22 summary.openOpportunityAmount = 0;
23
24 List<Account> accounts = [
25 SELECT Id, Name
26 FROM Account
27 WHERE Name LIKE :('%' + accountName + '%')
28 ORDER BY Name
29 LIMIT 1
30 ];
31
32 if (accounts.isEmpty()) {
33 return summary;
34 }
35
36 Account acc = accounts[0];
37 summary.accountName = acc.Name;
38 summary.accountInitials = toInitials(acc.Name);
39
40 List<String> stageSummaryParts = new List<String>();
41 for (AggregateResult ar : [
42 SELECT StageName, COUNT(Id) opportunityCount
43 FROM Opportunity
44 WHERE AccountId = :acc.Id
45 GROUP BY StageName
46 ORDER BY StageName
47 ]) {
48 String stageName = (String) ar.get('StageName');
49 Integer count = (Integer) ar.get('opportunityCount');
50
51 summary.totalOpportunityCount += count;
52 stageSummaryParts.add(stageName + ': ' + count + '件');
53 }
54 summary.stageSummaryText = String.join(stageSummaryParts, ' / ');
55
56 AggregateResult openTotal = [
57 SELECT SUM(Amount) totalAmount
58 FROM Opportunity
59 WHERE AccountId = :acc.Id AND IsClosed = false
60 ];
61 Decimal openAmount = (Decimal) openTotal.get('totalAmount');
62 summary.openOpportunityAmount = (openAmount != null) ? openAmount : 0;
63
64 summary.totalOpportunityCountText = summary.totalOpportunityCount + '件';
65 summary.openOpportunityAmountFormatted = formatCurrency(summary.openOpportunityAmount);
66
67 return summary;
68 }
69
70 // ウィジェット表示用に3桁区切りの「¥」表記へ整形する(Decimal.format()はロケール依存のため使わない)
71 private static String formatCurrency(Decimal amount) {
72 Long rounded = amount.round();
73 String sign = (rounded < 0) ? '-' : '';
74 String digits = String.valueOf(Math.abs(rounded));
75
76 List<String> groups = new List<String>();
77 Integer len = digits.length();
78 Integer head = Math.mod(len, 3);
79 if (head > 0) {
80 groups.add(digits.substring(0, head));
81 }
82 for (Integer i = head; i < len; i += 3) {
83 groups.add(digits.substring(i, i + 3));
84 }
85
86 return sign + '¥' + String.join(groups, ',');
87 }
88
89 // アバター表示用に、法人格の接頭辞を除いた先頭1文字を取り出す
90 private static String toInitials(String name) {
91 if (String.isBlank(name)) {
92 return '?';
93 }
94 String bareName = name.replace('株式会社', '').replace('有限会社', '').trim();
95 return String.isBlank(bareName) ? name.substring(0, 1) : bareName.substring(0, 1);
96 }
97
98 global class SummaryRequest {
99 @InvocableVariable(label='取引先名' description='検索対象の取引先名(部分一致)' required=true)
100 public String accountName;
101 }
102
103 global class SummaryResponse {
104 @InvocableVariable(label='商談サマリー')
105 public Summary summary;
106 }
107
108 global class Summary {
109 @InvocableVariable public String accountName;
110 @InvocableVariable public String accountInitials;
111 @InvocableVariable public Integer totalOpportunityCount;
112 @InvocableVariable public String stageSummaryText;
113 @InvocableVariable public Decimal openOpportunityAmount;
114 @InvocableVariable public String totalOpportunityCountText;
115 @InvocableVariable public String openOpportunityAmountFormatted;
116 }
117}ステップ 2:LightningType で型とレンダラーを定義する
ここが HXL の肝です。Apex の戻り値を、そのままウィジェットに流し込めるわけではありません。LightningType が「どの Apex 型を、どのウィジェットの、どの属性にマッピングするか」を仲介します。
MCP のレスポンスは 3 層の入れ子(エンベロープ)になっています。
1accountOpportunitySummaryResult ← 一番外側(actionName / isSuccess / outputValues)
2 └─ accountOpportunitySummaryOutputValues ← 中間(summary を保持)
3 └─ AccountOpportunitySummary$Summary ← Apex の Summary クラス(実データ)この 3 層に対応する LightningType をそれぞれ用意します。
2-1. 一番内側:Apex 型のラッパー
lightningTypes/accountOpportunitySummary/schema.json は、Apex の Summary クラスを LightningType として参照するだけのシンプルなものです。
1{
2 "title": "Account Opportunity Summary",
3 "description": "Apex から返される取引先の商談サマリー情報",
4 "lightning:type": "@apexClassType/c__AccountOpportunitySummary$Summary"
5}@apexClassType/c__<クラス名>$<内部クラス名> という記法で、Apex の内部クラスを直接参照できます。
2-2. 中間層:OutputValues
lightningTypes/accountOpportunitySummaryOutputValues/schema.json は、Apex アクションのレスポンス payload(summary を持つ)を表します。lightning:tags に mcp を付けるのがポイントです。
1{
2 "title": "Account Opportunity Summary Output Values",
3 "type": "object",
4 "lightning:type": "lightning__objectType",
5 "unevaluatedProperties": false,
6 "lightning:tags": [ "mcp" ],
7 "properties": {
8 "summary": {
9 "title": "Summary",
10 "lightning:type": "@apexClassType/c__AccountOpportunitySummary$Summary"
11 }
12 }
13}2-3. 一番外側:Result エンベロープ
lightningTypes/accountOpportunitySummaryResult/schema.json が MCP に公開される最上位の型です。actionName / isSuccess / outputValues を持ちます。
1{
2 "title": "Account Opportunity Summary Result",
3 "type": "object",
4 "lightning:type": "lightning__objectType",
5 "unevaluatedProperties": false,
6 "lightning:tags": [ "mcp" ],
7 "properties": {
8 "actionName": { "title": "Action Name", "lightning:type": "lightning__textType" },
9 "isSuccess": { "title": "Is Success", "lightning:type": "lightning__booleanType" },
10 "outputValues": {
11 "title": "Output Values",
12 "lightning:type": "c__accountOpportunitySummaryOutputValues"
13 }
14 }
15}2-4. レンダラー:属性のマッピング
そして最も重要なのが renderer.json です。これが 「入れ子になったデータのどこを、ウィジェットのどの属性に流し込むか」 を定義します。Result 型のレンダラーは、3 層をたどって値を取り出します。
1{
2 "renderer": {
3 "componentOverrides": {
4 "$": {
5 "definition": "@widget/c/accountSummaryCard",
6 "attributes": {
7 "accountName": "{!$attrs.outputValues.summary.accountName}",
8 "accountInitials": "{!$attrs.outputValues.summary.accountInitials}",
9 "totalOpportunityCountText": "{!$attrs.outputValues.summary.totalOpportunityCountText}",
10 "stageSummaryText": "{!$attrs.outputValues.summary.stageSummaryText}",
11 "openOpportunityAmountFormatted": "{!$attrs.outputValues.summary.openOpportunityAmountFormatted}"
12 }
13 }
14 }
15 }
16}"definition": "@widget/c/accountSummaryCard"で、次のステップで作るウィジェットを指定します。{!$attrs.outputValues.summary.xxx}という式で、エンベロープの奥にある実データを取り出してウィジェットの属性に渡します。この階層(outputValues.summary.)を間違えると値が表示されないので注意してください。
ステップ 3:UiWidgetBundle でウィジェットの UI を作る
いよいよ見た目です。HXL のウィジェットは tile/* というコンポーネントを JSON でツリー状に組み合わせて作ります。
3-1. スキーマ(受け取る属性の宣言)
uiWidgets/accountSummaryCard/schema.json で、ウィジェットが受け取る属性を宣言します。
1{
2 "title": "Account Opportunity Summary Widget",
3 "type": "object",
4 "properties": {
5 "attributes": {
6 "lightning:type": "lightning__objectType",
7 "properties": {
8 "accountName": { "title": "取引先名", "lightning:type": "lightning__textType" },
9 "accountInitials": { "title": "取引先名イニシャル", "lightning:type": "lightning__textType" },
10 "stageSummaryText": { "title": "フェーズ別商談数", "lightning:type": "lightning__textType" },
11 "totalOpportunityCountText": { "title": "商談数(表示用)", "lightning:type": "lightning__textType" },
12 "openOpportunityAmountFormatted": { "title": "未クローズ商談合計金額(表示用)", "lightning:type": "lightning__textType" }
13 }
14 }
15 }
16}💡 フェーズ別件数は、Apex 側で
フェーズA: 3件 / フェーズB: 2件のような 1 本のテキスト(stageSummaryText)に整形して渡しています。
3-2. ウィジェット本体(tile ツリー)
uiWidgets/accountSummaryCard/accountSummaryCard.json がレイアウト定義です。全体は長いので、構造を先に示します。
1tile/widget
2└─ tile/container
3 └─ tile/column (gap: lg)
4 ├─ [ヘッダー行] tile/row (justify: between)
5 │ ├─ tile/row → tile/avatar(イニシャル) + tile/column(取引先名 + キャプション)
6 │ └─ tile/badge(商談数)
7 ├─ [統計行] tile/row (align: stretch, isWrapped)
8 │ ├─ tile/container → icon(briefcase) + 「商談数」 + 大きな数値
9 │ └─ tile/container → icon(dollar-sign) + 「未クローズ商談合計金額」 + 大きな金額
10 ├─ tile/separator
11 └─ [フェーズ別] tile/column → 見出し + stageSummaryTextヘッダー部分(アバター+取引先名+商談数バッジ)の JSON は次のようになります。
1{
2 "definition": "tile/row",
3 "attributes": { "gap": "md", "align": "center", "justify": "between", "isWrapped": true },
4 "children": [
5 {
6 "definition": "tile/row",
7 "attributes": { "gap": "sm", "align": "center" },
8 "children": [
9 {
10 "definition": "tile/avatar",
11 "attributes": {
12 "initials": "{!$attrs.accountInitials}",
13 "alt": "{!$attrs.accountName}",
14 "size": "md",
15 "shape": "circle"
16 }
17 },
18 {
19 "definition": "tile/column",
20 "attributes": { "gap": "xs" },
21 "children": [
22 { "definition": "tile/text", "attributes": { "text": "{!$attrs.accountName}", "variant": "h3", "weight": "semibold" } },
23 { "definition": "tile/text", "attributes": { "text": "取引先の商談サマリー", "variant": "caption", "color": "muted" } }
24 ]
25 }
26 ]
27 },
28 {
29 "definition": "tile/badge",
30 "attributes": { "label": "{!$attrs.totalOpportunityCountText}", "variant": "primary" }
31 }
32 ]
33}未クローズ商談合計金額を強調して表示する統計カード部分はこうです。アイコンと大きな数値を組み合わせます。
1{
2 "definition": "tile/container",
3 "children": [
4 {
5 "definition": "tile/column",
6 "attributes": { "gap": "xs" },
7 "children": [
8 {
9 "definition": "tile/row",
10 "attributes": { "gap": "sm", "align": "center" },
11 "children": [
12 { "definition": "tile/icon", "attributes": { "name": "dollar-sign", "size": "sm", "color": "success" } },
13 { "definition": "tile/text", "attributes": { "text": "未クローズ商談合計金額", "variant": "caption", "color": "muted" } }
14 ]
15 },
16 {
17 "definition": "tile/text",
18 "attributes": { "text": "{!$attrs.openOpportunityAmountFormatted}", "variant": "h2", "weight": "bold", "color": "success" }
19 }
20 ]
21 }
22 ]
23}⚠️ 現時点でのハマりどころ:
tile/iconのnameは実在する値のみ
tile/iconのnameに存在しないアイコン名(例:currency)を指定すると、ウィジェット全体がNullPointerExceptionでクラッシュします。エラーメッセージが「An unexpected error occurred」としか出ないため、原因の特定に時間がかかりました。briefcaseやdollar-signなど、実在するアイコン名を使ってください。
UiWidgetBundle のメタデータ(accountSummaryCard.uiwidget-meta.xml)は widgetType を JSON にしておきます。
1<?xml version="1.0" encoding="UTF-8"?>
2<UiWidgetBundle xmlns="http://soap.sforce.com/2006/04/metadata">
3 <masterLabel>Account Summary Card</masterLabel>
4 <description>取引先名、商談数、フェーズ別商談数、未クローズ商談の合計金額を表示します。</description>
5 <widgetType>JSON</widgetType>
6</UiWidgetBundle>ステップ 4:GenAiFunction(Agent Action)で Apex を登録する
ここは見落としがちですが必須です。Apex クラスを MCP サーバーから呼び出せるようにするために、Agent アクションとして登録する必要があります。
genAiFunctions/Get_Account_Opportunity_Summary/Get_Account_Opportunity_Summary.genAiFunction-meta.xml:
1<?xml version="1.0" encoding="UTF-8"?>
2<GenAiFunction xmlns="http://soap.sforce.com/2006/04/metadata">
3 <description>取引先名から、商談件数・フェーズ別件数・未クローズ商談の合計金額を取得します。</description>
4 <developerName>Get_Account_Opportunity_Summary</developerName>
5 <invocationTarget>AccountOpportunitySummary</invocationTarget>
6 <invocationTargetType>apex</invocationTargetType>
7 <isConfirmationRequired>false</isConfirmationRequired>
8 <masterLabel>取引先の商談サマリーを取得</masterLabel>
9</GenAiFunction>入力スキーマ(input/schema.json)では、accountName をユーザー入力として受け取ることを宣言します。
1{
2 "required": [ "accountName" ],
3 "properties": {
4 "accountName": {
5 "title": "取引先名",
6 "lightning:type": "lightning__textType",
7 "copilotAction:isUserInput": true
8 }
9 },
10 "lightning:type": "lightning__objectType"
11}出力スキーマ(output/schema.json)では、summary を 表示可能(isDisplayable) として宣言します。これがウィジェット表示につながる重要なフラグです。
1{
2 "properties": {
3 "summary": {
4 "title": "商談サマリー",
5 "lightning:type": "@apexClassType/c__AccountOpportunitySummary$Summary",
6 "copilotAction:isDisplayable": true,
7 "copilotAction:isUsedByPlanner": true
8 }
9 },
10 "lightning:type": "lightning__objectType"
11}ステップ 5:McpServerDefinition で MCP サーバーとして公開する
最後に、これまでの部品を束ねる McpServerDefinition です。ここが「データ取得(tools)」と「見た目(resources)」を接続する結節点になります。
1<?xml version="1.0" encoding="UTF-8"?>
2<McpServerDefinition xmlns="http://soap.sforce.com/2006/04/metadata">
3 <description>取引先の商談サマリー(商談数、フェーズ別件数、未クローズ商談合計金額)を検索して返します。</description>
4 <masterLabel>account-opportunity-summary-server</masterLabel>
5 <tools>
6 <apiDefinition>
7 <apiIdentifier>aa:apex-AccountOpportunitySummary</apiIdentifier>
8 <apiSource>API_CATALOG</apiSource>
9 <operation>AccountOpportunitySummary</operation>
10 </apiDefinition>
11 <descriptionOverride>取引先名から、商談件数・フェーズ別件数・未クローズ商談の合計金額を取得します。</descriptionOverride>
12 <destructive>false</destructive>
13 <idempotent>true</idempotent>
14 <openWorld>false</openWorld>
15 <readOnly>true</readOnly>
16 <returnDirect>false</returnDirect>
17 <toolName>AccountOpportunitySummaryapex_AccountOpportunitySummary</toolName>
18 <toolTitle>AccountOpportunitySummary</toolTitle>
19 <uiResource>accountSummary</uiResource>
20 </tools>
21 <resources>
22 <resourceName>accountSummary</resourceName>
23 <resourceUri>ui://widget/lightningType/c__accountOpportunitySummaryResult</resourceUri>
24 <resourceTitle>Account Opportunity Summary</resourceTitle>
25 <description>取引先名、商談数、フェーズ別商談数、未クローズ商談合計金額を表示するウィジェット。</description>
26 </resources>
27</McpServerDefinition>読み解きのポイント
<tools>は データ取得の経路です。apiIdentifierのaa:apex-<クラス名>とapiSourceのAPI_CATALOGが、ステップ 4 で登録した Agent Action を指しています。<resources>は 見た目の経路です。resourceUriはui://widget/lightningType/c__<LightningTypeBundleのAPI名>という形式で、ステップ 2 で作った Result 型を指します。<uiResource>と<resourceName>の値(accountSummary)を一致させることで、ツールとリソース(=データと見た目)が紐づきます。
💡 現時点の仕様: CSP(
connectDomainsなど)や_meta.ui.domainといった MCP プロトコルの内部設定は、Salesforce プラットフォームが自動生成します。開発者がメタデータで設定する項目ではありません。
ステップ 6:デプロイする
ソースを組織にデプロイします。依存関係の順(Apex → LightningType & UiWidget → GenAiFunction → McpServerDefinition)を意識するとエラーが減りますが、まとめてデプロイしても解決してくれることが多いです。
1sf project deploy start \
2 --source-dir force-app/main/default \
3 --target-org <your-org-alias> \
4 --wait 15 --json現時点の仕様: ⚠️ UiWidgetBundle のスキーマは「プロパティ削除」で失敗する
一度デプロイしたウィジェットのschema.jsonから既存プロパティを削除すると、Schema update contains breaking changes: ... プロパティは削除できませんというエラーになります。属性を作り直したいときは、古いプロパティを残したまま新しいものを追加するのが安全です。
ステップ 7:MCP サーバーを有効化し、AI エージェントアプリから接続する
デプロイが終わったら、MCP サーバーを有効化します。これは現状 設定画面からの手動操作のみです。
有効化したら、外部クライアントアプリケーションを作成し、AI エージェントアプリ側でコネクタ(プラグイン)として登録します。この OAuth まわりの手順は、別記事「Salesforce Hosted MCP Server 使い始め」で詳しく解説されているので、そちらもあわせてご覧ください。
ChatGPT と Claude で試してみる
公開した MCP サーバーは、MCP に対応した AI エージェントアプリから接続できます。コネクタとして登録したうえで「SalesforceDev で United Oil の商談サマリを見せて」と話しかけると、AccountOpportunitySummary ツールが呼び出され、次のように同じカードが表示されます。
Claude の場合
ChatGPT の場合
SUM(Amount) も GROUP BY StageName も、すべて組織の実データが反映されています。フィールドレベルセキュリティ(FLS)や共有ルールも尊重されるため、安心して社内データを扱えます。同じ MCP サーバーを Claude と ChatGPT のどちらから呼び出しても、同一のウィジェットが同じ見た目で表示される点にも注目してください。
要考慮事項:ウィジェットの変更が反映されない
ところで、ウィジェットの定義を更新後、デプロイは成功しているのに、AI エージェントアプリ側では古いレイアウトのままという現象に悩まされました。組織側のメタデータを取得して確認しても、ローカルと内容は一致しています。それでも新しい見た目にならないのです。
原因は、AI エージェントアプリ(クライアント)側が、ウィジェットのテンプレートを resourceUri の文字列をキーにキャッシュしているからのようです。AI エージェントアプリの内部仕様で詳細は不明ですがresourceUri が同じままだと、中身をいくら変えても古いテンプレートが使われ続けるように見えました。
実際、クライアント側のコネクタ設定を見ると、ui://widget/lightningType/c__accountOpportunitySummary... という resourceUri をキーに、ウィジェットのテンプレート内容が保持されているのが確認できます。
ひとまずの対策は resourceUri のバージョニングです。
- LightningTypeBundle を新しい API 名でコピーする(例:
accountOpportunitySummaryResult→accountOpportunitySummaryResultV2) McpServerDefinitionの<resourceUri>と<uiResource>を新しいものに更新する- デプロイ後、AI エージェントアプリ側でコネクタ/プラグインを更新(再登録)する
1<!-- 変更前 -->
2<resourceUri>ui://widget/lightningType/c__accountOpportunitySummaryResult</resourceUri>
3
4<!-- 変更後(V2 に)-->
5<resourceUri>ui://widget/lightningType/c__accountOpportunitySummaryResultV2</resourceUri>なお、バージョニングが必要なのは resourceUri が指す LightningTypeBundle と McpServerDefinition だけです。UiWidgetBundle は同じ名前のまま上書きデプロイで問題ありません(クライアントは URI をキーにキャッシュしているため、URI が変われば中身も取り直されます)。Apex や GenAiFunction も影響を受けません。
ただし、この対策はあまり美しいものとは言えません。今後の AI エージェントアプリ側でのキャッシュクリアなどよりスマートな解決策が出てくることを願っています。
おわりに
本記事では、Apex・LightningType・UiWidgetBundle・GenAiFunction・McpServerDefinition を組み合わせて、組織の実データ(取引先の商談サマリー)を AI エージェントアプリにリッチなカードとして表示する HXL カスタムウィジェットを作りました。
従来、AI エージェントアプリに返せるのはテキストや静的なサンプルが中心でした。HXL と MCP を使うことで、Salesforce の実データを、意味のある UI として、外部の AI 体験に直接埋め込めるようになります。FLS や共有ルールも尊重されるため、セキュリティ面でも安心です。
一度この 7 ステップの型を押さえてしまえば、Apex のクエリとウィジェットの tile ツリーを差し替えるだけで、ケース一覧・在庫状況・売上ダッシュボードなど、さまざまなカスタムウィジェットに応用できます。ぜひ Developer Edition で試してみてください。



