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?

ServiceNow MCP に SAM Proツールを追加してみた

0
Last updated at Posted at 2026-07-01

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_entriesrisklist_software_lifecycle_reportscurrent_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 Highvery_high / lifecycle_phase=End of lifeend_of_life / current_phase=End of extended supportend_of_extended_support の変換とフィルタ成功を確認済みです。

教訓: sys_dictionaryinternal_typestring でも、sys_choice で運用されているフィールドは実機でラベルと値のマッピングを確認する。
type だけ見て「string だから素通しで良い」と判断しない。

おまけ: reference フィールドへの LIKE とドットウォーク

list_software_models では、manufacturer(パブリッシャー)や product(製品名)がどちらも reference フィールドです。ServiceNow の encoded query は reference フィールドに対して LIKE を直接書くと表示値に対する部分一致として解釈してくれる一方、確実性を優先してドットウォーク(manufacturer.nameLIKEproduct.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 から触るときに効いた知見:

  1. display_value: true は reference だけでなく currency/date も整形する。 数値計算をする集計ロジックでは使わない
  2. sys_dictionary.internal_typestring でも sys_choice 運用のフィールドはラベル≠格納値。 実機の sys_choice テーブルで確認してから正規化マップを用意する
  3. reference フィールドのフィルタは ドットウォーク(field.nameLIKE...)で明示するのが確実
  4. ELP・ライフサイクルレポートは自動計算値なので 書き込みツールは作らない(読み取り専用に限定する設計判断)

smart_query の日本語トークン化バグに続き、今回も「ドキュメントだけ読んでいたら気づかなかった」実機由来のバグでした。
AIに sys_dictionary / sys_choice を実機で叩かせながら型と値の実態を確認する進め方は、ServiceNow のような「フィールド定義と実データの挙動が一致しない」プラットフォームと相性が良いと感じています。

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?