はじめに
前回の記事では ServiceNow MCP サーバーの概要と基本的なセットアップ方法を紹介しました。
今回は新しい機能 describe_table を追加したので、実装の詳細とハマりポイントを紹介します。
ServiceNow MCP(Model Context Protocol)サーバーを使って AI に ServiceNow を操作させていると、こんな場面によく遭遇します。
「このカスタムテーブルにどんなフィールドがある?」
「incidentはtaskを継承しているはずだけど、task側のフィールドも含めて見たい」
「sn_vul_vulnerable_itemのrisk_scoreカラムは integer? string型?」
既存の get_table_schema 機能はテーブルにレコードが存在しないと空を返すことがあり、新規テーブルなどで空レコードの調査では向きませんでした。
そこで sys_dictionary テーブルを直接参照する describe_table 機能を実装しました。
追加した機能
describe_table
概要: sys_dictionary を使ってテーブルのフィールドスキーマを返す。
レコード件数に依存せず、カスタムテーブルや空テーブルでも正確に動作する。
引数:
| 引数 | 型 | 必須 | 説明 |
|---|---|---|---|
table |
string | ✅ | テーブル名(例: sn_vul_vulnerable_item) |
include_inherited |
boolean | — | 親テーブルのフィールドも含める(デフォルト: false) |
レスポンス例(sn_vul_vulnerable_item):
{
"table": "sn_vul_vulnerable_item",
"label": "Vulnerable Item",
"field_count": 94,
"fields": [
{ "element": "active", "column_label": "Active", "type": "boolean", "mandatory": false, "unique": false },
{ "element": "assigned_to", "column_label": "Assigned to", "type": "reference", "reference": "sys_user", "mandatory": false, "unique": false },
{ "element": "risk_score", "column_label": "Risk score", "type": "integer", "mandatory": false, "unique": false },
...
],
"summary": "Table \"sn_vul_vulnerable_item\" has 94 field(s)"
}
include_inherited: true のレスポンス例(incident):
{
"table": "incident",
"label": "Incident",
"parent_table": "task",
"field_count": 95,
"fields": [
{ "element": "assigned_to", "type": "reference", "reference": "sys_user", "defined_in": "task", ... },
{ "element": "category", "type": "string", "defined_in": "incident", ... },
...
],
"summary": "Table \"incident\" has 95 field(s) (includes inherited from \"task\")"
}
include_inherited: true のとき各フィールドに defined_in が付き、どのテーブルで定義されたかが分かります。
実装のポイント
1. sys_dictionary で空テーブルでも確実に取得
const dictResp = await client.queryRecords({
table: 'sys_dictionary',
query: `name=${tableName}^internal_type!=collection^element!=NULL`,
fields: 'element,column_label,internal_type,reference,mandatory,unique,name',
limit: 500,
});
internal_type!=collection でメタ行を除外、element!=NULL でテーブル定義行本体を除外しています。
2. reference フィールドは {value, link} オブジェクトで返ってくる
ServiceNow Table API は参照型フィールドを文字列ではなくオブジェクトで返します。
const strVal = (v: unknown): string =>
v && typeof v === 'object' ? ((v as any).value ?? '') : (v as string) ?? '';
この変換を挟まないと reference: "[object Object]" になります。
3. 親テーブル解決のバグ(super_class の罠)
include_inherited: true で親テーブル名を取得しようとしたとき、最初の実装は次のように書いていました。
// ❌ 動かない実装
const parentTable = dbObj.super_class?.display_value;
しかし実際のレスポンスは display_value を含まず、sys_id だけが返ってきます。
{
"super_class": {
"value": "85fb2e42187232108bb255f46a373ab3",
"link": "https://dev400464.service-now.com/api/now/table/sys_db_object/85fb2e42187232108bb255f46a373ab3"
}
}
解決には sys_id を使って sys_db_object を再クエリして名前を解決する必要があります。
const superClassSysId =
dbObj.super_class && typeof dbObj.super_class === 'object'
? (dbObj.super_class as any).value || undefined
: undefined;
if (includeInherited && superClassSysId) {
const parentResp = await client.queryRecords({
table: 'sys_db_object',
query: `sys_id=${superClassSysId}`,
fields: 'name',
limit: 1,
});
parentTable = parentResp.records[0]?.name;
}
display_value は sysparm_display_value=true を付けたときにのみ返るオプション値で、デフォルトでは含まれません。
sysparm_display_value=true を渡せば super_class.display_value に親テーブル名が直接返るため、2回目のクエリは不要になります。現在の queryRecords はこのパラメータに未対応のため再クエリで解決していますが、将来的には QueryRecordsParams に display_value オプションを追加して1クエリ化する予定です。
実機確認結果(dev PDI)
| テスト | 結果 |
|---|---|
sn_vul_vulnerable_item(フィールドなしでも取得可能) |
94フィールド ✅ |
incident(include_inherited: true) |
task継承70件 + incident固有25件 = 95件 ✅ |
sn_sec_cvd_source_config(CVDB設定テーブル) |
12フィールド、priority/source が MANDATORY と確認 ✅ |
| 存在しないテーブル |
Table "nonexistent_xyz" not found in sys_db_object エラー ✅ |
テスト
バグ修正のリグレッションテストも追加しています。
it('resolves parent table name from super_class sys_id when include_inherited is true', async () => {
// real API returns {value: sys_id, link: ...}, NOT display_value
(mockClient.queryRecords as ReturnType<typeof vi.fn>)
.mockResolvedValueOnce({
count: 1,
records: [{ name: 'incident', label: 'Incident',
super_class: { value: 'task-sys-id-001', link: '...' } }],
})
// Second call: resolve sys_id → parent table name
.mockResolvedValueOnce({ count: 1, records: [{ name: 'task' }] })
.mockResolvedValueOnce({ count: 1, records: [/* incident fields */] })
.mockResolvedValueOnce({ count: 1, records: [/* task fields */] });
const result = await executeCoreToolCall(mockClient, 'describe_table',
{ table: 'incident', include_inherited: true });
expect(result.parent_table).toBe('task');
// 4 calls: sys_db_object(incident) + sys_db_object(parent resolve) + dict(incident) + dict(task)
expect(mockClient.queryRecords).toHaveBeenCalledTimes(4);
});
まとめ
| 観点 |
get_table_schema(既存) |
describe_table(今回追加) |
|---|---|---|
| 空テーブルの扱い | フィールドが返らないことがある |
sys_dictionary 参照なので必ず返る |
| カスタムテーブル | △ | ✅ |
| 参照先テーブル名 | — | ✅(reference フィールドに表示) |
| 親テーブルのフィールド | — | ✅(include_inherited: true) |
| mandatory / unique フラグ | — | ✅ |
AI に ServiceNow のテーブルを自律探索させる際の基盤ツールとして追加しました。
新規機能(CVDB や USEM 系の新規テーブルなど)を調べるときに有効です。