ServiceNow を AI から自然言語で操作する MCP サーバーに、SAM Pro(Software Asset Management) 専用のツールを追加しました。
ソフトウェアインストール・ソフトウェア検出モデル・製品カタログ・ライセンスコンプライアンス(ELP)・ソフトウェアモデル・EOL/EOS ライフサイクルまで、11 ツールで AI から扱えます。
現状、ITAM ツールに alm_license ベースの簡易版は提供しているが、SAM Pro の正規化されたソフトウェア・コンプライアンスデータはカバーしていません。
実機(PDI)でテーブルを確認しながら実装したところ、途中で2つの実データ由来のバグを自分で埋め込み、また実機検証で見つけました。この記事はその共有が主目的です。
GitHub: https://github.com/tedorigawa001/ServiceNow-MCP
何を追加したか
src/tools/sam.ts に11ツールを実装しました。
既存の itam_analyst パッケージに統合しています(12 → 23 ツール)。
| テーブル | 用途 | 主なツール |
|---|---|---|
cmdb_sam_sw_install |
ソフトウェアインストール |
list_software_installs / get_software_install
|
samp_sw_product |
製品カタログ | list_software_products |
samp_license_position_report |
ELP(Effective License Position) |
list_license_positions / get_license_position_summary
|
cmdb_sam_sw_discovery_model |
ソフトウェア検出モデル | list_software_discovery_models |
cmdb_software_product_model |
ソフトウェア製品モデル(バージョン/エディション単位のエンタイトルメント) |
list_software_models / get_software_model
|
sam_sw_product_lifecycle_report |
製品ごとの EOL/EOS フェーズ・CVE 露出レポート |
list_software_lifecycle_reports / get_software_lifecycle_report
|
sam_sw_product_lifecycle |
ライフサイクル マスターデータ(パブリッシャー提供の GA/EOL/EOS フェーズ) | list_software_lifecycle_entries |
すべて Tier 0(読み取り専用) です。
ELP やライフサイクルレポートの数値は SAM Pro の突合バッチが計算するもので、ユーザーが直接編集する対象ではないため、書き込みツールは意図的に実装していません。
ここからが本題です。
落とし穴1: display_value: true が通貨フィールドを文字列に変える
get_license_position_summary(ELP の集計ダッシュボード)を実装したとき、他ツールに合わせてリファレンスフィールドを読みやすくするため display_value: true を付けました。
// ❌ バグ版
const resp = await client.queryRecords({
table: 'samp_license_position_report',
display_value: true,
fields: 'over_licensed_amount,potential_savings,true_up_cost,...',
});
const over = Number(r.over_licensed_amount); // NaN!
実機で叩くと、集計が 常に 0 件 になりました。原因はレスポンスの中身です。
display_value なし: "over_licensed_amount": "3855035.2455"
display_value あり: "over_licensed_amount": "$3,855,035.2455" ← 通貨記号とカンマ付き文字列
display_value: true は reference フィールドの表示名だけでなく、通貨(currency)フィールドも整形済み文字列に変えていました。
Number("$3,855,035.2455") は当然 NaN になり、over > 0 の判定が常に false になっていたわけです。
修正: 集計用クエリだけ display_value を外す
reference フィールドの読みやすさが欲しいのは一覧表示系のツールだけで、集計ロジックには生の数値文字列で十分です。
// ✅ 修正版: 集計用は display_value を付けない
const resp = await client.queryRecords({
table: 'samp_license_position_report',
query,
limit: 1000,
// 通貨フィールドが "$3,855,035.24" のような文字列になり Number() が壊れるため省略
fields: 'product,publisher,licenses_owned,licenses_required,over_licensed_amount,potential_savings,true_up_cost,status',
});
修正後、実機で 141 製品中、過剰ライセンス 78 件・要追加購入(true-up)28 件・節約可能額 $480,899・true-up 総額 約 $1,771 万 という妥当な集計が取れました。
教訓: display_value: true は reference だけでなく currency/date 系のあらゆる表示整形に影響する。数値計算をする経路では使わない。
落とし穴2: sys_choice は表示ラベルと内部値が別物
ライフサイクル関連の2ツール(list_software_lifecycle_entries の risk、list_software_lifecycle_reports の current_phase)にフィルタ引数を実装したとき、人間が読む文字列をそのままクエリに渡しました。
// ❌ バグ版
if (args.risk) query = `risk=${args.risk}`;
// executeSamToolCall(client, 'list_software_lifecycle_entries', { risk: 'Very High' })
実機で叩くと 0 件。risk フィールドは sys_dictionary 上は string 型なのに、実際は sys_choice の選択肢リストで運用されていて、ラベルと格納値が違いました。
sys_choice(name=sam_sw_product_lifecycle, element=risk):
label: "Very High" → value: "very_high"
label: "High" → value: "high"
label: "Moderate" → value: "moderate"
display_value: true を付けてレコードを読むと risk にラベル("Very High")がそのまま返るので一見矛盾しませんが、encoded query のフィルタ条件には内部値が必要です。lifecycle_phase(GA/EOL/EOS/EOES)も同じ構造で、"End of life" ではなく "end_of_life" を渡す必要がありました。
修正: ラベル→内部値の正規化マップ
const RISK_CHOICES: Record<string, string> = {
none: 'none', low: 'low', moderate: 'moderate', medium: 'moderate',
high: 'high', 'very high': 'very_high',
};
function normalizeChoice(input: string, map: Record<string, string>): string {
const key = input.trim().toLowerCase().replace(/[_-]+/g, ' ').replace(/\s+/g, ' ');
return map[key] ?? input; // マップに無ければそのまま通す(既に内部値かもしれない)
}
これで risk: 'Very High'(表示ラベル)でも risk: 'very_high'(内部値)でもどちらでも動くようになりました。フォールバックで元の入力をそのまま返すため、想定外の値を渡してもエラーにはならず、単に ServiceNow 側で 0 件になるだけです(安全側に倒しています)。
実機で risk=Very High → very_high / lifecycle_phase=End of life → end_of_life / current_phase=End of extended support → end_of_extended_support の変換とフィルタ成功を確認済みです。
教訓: sys_dictionary の internal_type が string でも、sys_choice で運用されているフィールドは実機でラベルと値のマッピングを確認する。
type だけ見て「string だから素通しで良い」と判断しない。
おまけ: reference フィールドへの LIKE とドットウォーク
list_software_models では、manufacturer(パブリッシャー)や product(製品名)がどちらも reference フィールドです。ServiceNow の encoded query は reference フィールドに対して LIKE を直接書くと表示値に対する部分一致として解釈してくれる一方、確実性を優先してドットウォーク(manufacturer.nameLIKE、product.prod_nameLIKE)で明示的に対象フィールドを指定しました。
if (args.publisher) query = `manufacturer.nameLIKE${args.publisher}`;
if (args.product) query = query ? `${query}^product.prod_nameLIKE${args.product}` : `product.prod_nameLIKE${args.product}`;
実機で manufacturer.nameLIKEMicrosoft が Windows Server / Exchange Server などを正しく返すことを確認済みです。
まとめ
SAM Pro のテーブルを API から触るときに効いた知見:
-
display_value: trueは reference だけでなく currency/date も整形する。 数値計算をする集計ロジックでは使わない -
sys_dictionary.internal_typeがstringでもsys_choice運用のフィールドはラベル≠格納値。 実機のsys_choiceテーブルで確認してから正規化マップを用意する - reference フィールドのフィルタは ドットウォーク(
field.nameLIKE...)で明示するのが確実 - ELP・ライフサイクルレポートは自動計算値なので 書き込みツールは作らない(読み取り専用に限定する設計判断)
smart_query の日本語トークン化バグに続き、今回も「ドキュメントだけ読んでいたら気づかなかった」実機由来のバグでした。
AIに sys_dictionary / sys_choice を実機で叩かせながら型と値の実態を確認する進め方は、ServiceNow のような「フィールド定義と実データの挙動が一致しない」プラットフォームと相性が良いと感じています。