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 に describe_table 機能を追加してみた

0
Posted at

はじめに

前回の記事では ServiceNow MCP サーバーの概要と基本的なセットアップ方法を紹介しました。

今回は新しい機能 describe_table を追加したので、実装の詳細とハマりポイントを紹介します。

ServiceNow MCP(Model Context Protocol)サーバーを使って AI に ServiceNow を操作させていると、こんな場面によく遭遇します。

「このカスタムテーブルにどんなフィールドがある?」
incidenttask を継承しているはずだけど、task 側のフィールドも含めて見たい」
sn_vul_vulnerable_itemrisk_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_valuesysparm_display_value=true を付けたときにのみ返るオプション値で、デフォルトでは含まれません。

sysparm_display_value=true を渡せば super_class.display_value に親テーブル名が直接返るため、2回目のクエリは不要になります。現在の queryRecords はこのパラメータに未対応のため再クエリで解決していますが、将来的には QueryRecordsParamsdisplay_value オプションを追加して1クエリ化する予定です。


実機確認結果(dev PDI)

テスト 結果
sn_vul_vulnerable_item(フィールドなしでも取得可能) 94フィールド ✅
incidentinclude_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 系の新規テーブルなど)を調べるときに有効です。

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?