mcpField
S
scripted-rest-apis

scripted-rest-apis

github.com/happy-technologies-llc開発4143 回インストール2 日前に更新
リポジトリ

scripted-rest-apis について

ServiceNow の開発者、インテグレーション担当者、管理者向けに、カスタム Scripted REST API の構築を解説するスキルです。エンドポイント設計、リクエストとレスポンス、セキュリティ、バージョン管理、テストを扱います。

主な機能

  1. 01カスタム REST エンドポイント設計
  2. 02リクエストとレスポンスの整形
  3. 03認証とアクセス制御
  4. 04クエリ・パスパラメーター処理
  5. 05エラー処理と API バージョン管理
  6. 06API Explorer と curl によるテスト

活用シーン

  1. 01外部システム向けの専用エンドポイントを公開
  2. 02API 操作に独自のビジネスロジックを実装
  3. 03複数テーブルのデータを集約
  4. 04アクセス制御付きのバージョン管理 API を定義

MCP 対応

ServiceNow のアプリケーションコンテキスト設定や API 定義・操作の作成、検索、更新に MCP ツールを使う手順を示します。

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/happy-technologies-llc/happy-platform-skills --skill scripted-rest-apis

インストール後、Claude Code で/scripted-rest-apisを入力して読み込まれたか確認します。

詳細

スクリプト型 REST API の開発

概要

このスキルでは、ServiceNow でカスタムのスクリプト型 REST API を開発するための包括的なガイドを提供します。スクリプト型 REST API を使用すると、次のことができます。

  • カスタムエンドポイントの公開 - 標準の Table API では実現できない、目的に特化した API を作成します
  • データ変換の制御 - 統合の要件に合わせてリクエスト/レスポンス形式を整えます
  • ビジネスロジックの実装 - 1 回の API 呼び出しで複雑な処理を実行します
  • 統合の保護 - きめ細かな認証と認可を適用します
  • 外部システムのサポート - サードパーティシステムとの統合用インターフェースを提供します

スクリプト型 REST API を使用する場合:

  • 標準の Table API では要件を満たせない
  • API レイヤーにカスタムのビジネスロジックが必要
  • 特定のリクエスト/レスポンス形式が必要
  • 外部システムに専用のエンドポイントが必要
  • 複数のテーブルからデータを集約する必要がある

対象者: カスタムの ServiceNow 統合を構築する開発者、統合スペシャリスト、管理者。

前提条件

  • ロール: admin、web_service_admin、またはスコープ付きアプリの開発者
  • 権限: sys_ws_definition、sys_ws_operation の作成/変更
  • 知識: JavaScript、REST の原則、HTTP メソッド、JSON
  • 環境: 開発インスタンス(本番環境で API を直接開発しないでください)
  • 関連スキル:
    • admin/update-set-management - API 定義の取り込み
    • admin/application-scope - スコープ付きアプリの API 開発
    • security/acl-management - API のセキュリティ制御

テーブルアーキテクチャ

プログラムで API を作成するには、基盤となるテーブルを理解することが不可欠です。

コアテーブル
sys_ws_definition (API Definition)
    |
    +-- sys_ws_operation (Resources/Endpoints)
            |
            +-- Request Headers (configured in operation)
            +-- Query Parameters (configured in operation)
            +-- Path Parameters (defined in relative_path)
            +-- Script (handles request/response)
主なリレーションシップ
テーブル目的主なフィールド
sys_ws_definitionAPI コンテナーname、namespace、base_path、active
sys_ws_operation個々のエンドポイントname、http_method、relative_path、script
sys_script_include再利用可能なロジックname、script、api_name
sys_ws_api_headerカスタムヘッダーname、api_id、direction
API URL の構造
https://<instance>.service-now.com/api/<namespace>/<api_name>/<version>/<resource>/<path_param>

Example:
https://dev12345.service-now.com/api/x_company/customer_api/v1/customers/12345
    |                                |     |            |       |           |
    Instance                     Namespace  API Name  Version Resource  Path Param

手順

フェーズ 1:API の設計

API を作成する前に、構造を慎重に計画してください。

ステップ 1.1:API の要件を定義する

API 仕様を文書化します。

API Name: Customer Integration API
Namespace: x_company (scoped app) or now (global)
Version: v1
Base Path: /api/x_company/customer_api

Resources:
- GET /customers - List all customers (with pagination)
- GET /customers/{id} - Get specific customer
- POST /customers - Create new customer
- PUT /customers/{id} - Update customer
- DELETE /customers/{id} - Delete customer
- POST /customers/{id}/orders - Create order for customer
ステップ 1.2:認証を計画する
認証タイプ使用例設定
Basic Authサービスアカウントデフォルトで有効
OAuth 2.0サードパーティアプリOAuth プロバイダーを設定
API Keyシンプルな統合カスタムヘッダーの検証
SessionブラウザベースCookie ベースの認証
Mutual TLS高いセキュリティ証明書の検証
フェーズ 2:API 定義の作成
ステップ 2.1:コンテキストの設定(スコープ付きアプリ)

スコープ付きアプリで作成する場合:

MCP を使用:

Tool: SN-Set-Current-Application
Parameters:
  app_sys_id: [your_app_sys_id]
Tool: SN-Set-Update-Set
Parameters:
  update_set_sys_id: [your_update_set_sys_id]
ステップ 2.2:API 定義の作成

MCP を使用:

Tool: SN-Create-Record
Parameters:
  table_name: sys_ws_definition
  data:
    name: Customer Integration API
    short_description: API for customer management and integration
    namespace: x_company
    doc_link: https://docs.company.com/api/customers
    active: true
    enforce_acl: true
    is_versioned: true
    baseline_version: v1
    published: false

主なフィールドの説明:

フィールド目的推奨事項
namespaceURL のセグメント。通常はアプリのスコープアプリのスコープを使用します(x_company)
enforce_acl基盤となるテーブルの ACL を確認セキュリティのため true に設定します
is_versionedAPI のバージョン管理を有効化常に true に設定します
baseline_versionバージョンが指定されていない場合のデフォルトバージョンv1 から始めます
publishedAPI を検出可能にする開発中は false に設定します

sys_id を保存します: api_definition_sys_id

ステップ 2.3:API 作成の確認
Tool: SN-Query-Table
Parameters:
  table_name: sys_ws_definition
  query: name=Customer Integration API
  fields: sys_id,name,namespace,active,service_address

service_address フィールドに API の完全な URL が表示されます。

フェーズ 3:リソース(オペレーション)の作成
ステップ 3.1:GET コレクションリソース(一覧)

ページネーション付きで顧客を一覧表示するエンドポイントを作成します。

MCP を使用:

Tool: SN-Create-Record
Parameters:
  table_name: sys_ws_operation
  data:
    name: List Customers
    web_service_definition: [api_definition_sys_id]
    http_method: GET
    relative_path: /customers
    operation_script: |
      (function process(/*RESTAPIRequest*/ request, /*RESTAPIResponse*/ response) {

        // Get query parameters for pagination and filtering
        var limit = parseInt(request.queryParams.limit) || 20;
        var offset = parseInt(request.queryParams.offset) || 0;
        var active = request.queryParams.active;
        var search = request.queryParams.q;

        // Validate limit
        if (limit > 100) limit = 100;
        if (limit < 1) limit = 20;

        // Build query
        var gr = new GlideRecord('customer');

        if (active !== undefined) {
          gr.addQuery('active', active === 'true');
        }

        if (search) {
          var qc = gr.addQuery('name', 'CONTAINS', search);
          qc.addOrCondition('email', 'CONTAINS', search);
          qc.addOrCondition('account_number', 'CONTAINS', search);
        }

        // Get total count before pagination
        var countGR = new GlideAggregate('customer');
        countGR.addAggregate('COUNT');
        if (active !== undefined) {
          countGR.addQuery('active', active === 'true');
        }
        countGR.query();
        var totalCount = 0;
        if (countGR.next()) {
          totalCount = parseInt(countGR.getAggregate('COUNT'));
        }

        // Apply pagination
        gr.orderBy('name');
        gr.chooseWindow(offset, offset + limit);
        gr.query();

        // Build response
        var customers = [];
        while (gr.next()) {
          customers.push({
            sys_id: gr.getUniqueValue(),
            name: gr.getValue('name'),
            email: gr.getValue('email'),
            account_number: gr.getValue('account_number'),
            active: gr.getValue('active') === 'true',
            created: gr.getValue('sys_created_on'),
            updated: gr.getValue('sys_updated_on')
          });
        }

        // Set response
        response.setStatus(200);
        response.setBody({
          result: customers,
          meta: {
            total: totalCount,
            limit: limit,
            offset: offset,
            has_more: (offset + limit) < totalCount
          }
        });

      })(request, response);
    short_description: Retrieve list of customers with pagination
    requires_acl_authorization: true
    requires_authentication: true
    requires_snc_internal_role: false

sys_id を保存します: list_customers_operation_id

ステップ 3.2:GET 単一リソース

特定の顧客を取得するエンドポイントを作成します。

MCP を使用:

Tool: SN-Create-Record
Parameters:
  table_name: sys_ws_operation
  data:
    name: Get Customer
    web_service_definition: [api_definition_sys_id]
    http_method: GET
    relative_path: /customers/{id}
    operation_script: |
      (function process(/*RESTAPIRequest*/ request, /*RESTAPIResponse*/ response) {

        // Get path parameter
        var customerId = request.pathParams.id;

        // Validate input
        if (!customerId) {
          response.setStatus(400);
          response.setBody({
            error: {
              message: 'Customer ID is required',
              code: 'MISSING_PARAMETER'
            }
          });
          return;
        }

        // Query customer
        var gr = new GlideRecord('customer');

        // Support both sys_id and account_number lookup
        if (customerId.length === 32) {
          gr.get(customerId);
        } else {
          gr.addQuery('account_number', customerId);
          gr.query();
          gr.next();
        }

        if (!gr.isValidRecord()) {
          response.setStatus(404);
          response.setBody({
            error: {
              message: 'Customer not found',
              code: 'NOT_FOUND',
              detail: 'No customer exists with ID: ' + customerId
            }
          });
          return;
        }

        // Check ACL (optional if enforce_acl is true on definition)
        if (!gr.canRead()) {
          response.setStatus(403);
          response.setBody({
            error: {
              message: 'Access denied',
              code: 'FORBIDDEN'
            }
          });
          return;
        }

        // Build detailed response
        var customer = {
          sys_id: gr.getUniqueValue(),
          name: gr.getValue('name'),
          email: gr.getValue('email'),
          phone: gr.getValue('phone'),
          account_number: gr.getValue('account_number'),
          address: {
            street: gr.getValue('street'),
            city: gr.getValue('city'),
            state: gr.getValue('state'),
            zip: gr.getValue('zip'),
            country: gr.getValue('country')
          },
          contacts: [],
          active: gr.getValue('active') === 'true',
          created: gr.getValue('sys_created_on'),
          updated: gr.getValue('sys_updated_on'),
          created_by: gr.getDisplayValue('sys_created_by'),
          updated_by: gr.getDisplayValue('sys_updated_by')
        };

        // Get related contacts
        var contacts = new GlideRecord('customer_contact');
        contacts.addQuery('customer', gr.getUniqueValue());
        contacts.query();
        while (contacts.next()) {
          customer.contacts.push({
            sys_id: contacts.getUniqueValue(),
            name: contacts.getValue('name'),
            email: contacts.getValue('email'),
            phone: contacts.getValue('phone'),
            primary: contacts.getValue('primary') === 'true'
          });
        }

        response.setStatus(200);
        response.setBody({ result: customer });

      })(request, response);
    short_description: Retrieve a specific customer by ID
    requires_acl_authorization: true
    requires_authentication: true
ステップ 3.3:POST リソース(作成)

新しい顧客を作成するエンドポイントを作成します。

MCP を使用:

Tool: SN-Create-Record
Parameters:
  table_name: sys_ws_operation
  data:
    name: Create Customer
    web_service_definition: [api_definition_sys_id]
    http_method: POST
    relative_path: /customers
    operation_script: |
      (function process(/*RESTAPIRequest*/ request, /*RESTAPIResponse*/ response) {

        // Parse request body
        var body = request.body.data;

        // Validate required fields
        var errors = [];
        if (!body.name) errors.push('name is required');
        if (!body.email) errors.push('email is required');

        // Validate email format
        if (body.email && !body.email.match(/^[^\s@]+@[^\s@]+\.[^\s@]+$/)) {
          errors.push('email format is invalid');
        }

        if (errors.length > 0) {
          response.setStatus(400);
          response.setBody({
            error: {
              message: 'Validation failed',
              code: 'VALIDATION_ERROR',
              details: errors
            }
          });
          return;
        }

        // Check for duplicate email
        var existing = new GlideRecord('customer');
        existing.addQuery('email', body.email);
        existing.query();
        if (existing.next()) {
          response.setStatus(409);
          response.setBody({
            error: {
              message: 'Customer with this email already exists',
              code: 'DUPLICATE_ERROR',
              existing_id: existing.getUniqueValue()
            }
          });
          return;
        }

        // Create customer
        var gr = new GlideRecord('customer');
        gr.initialize();

        // Required fields
        gr.setValue('name', body.name);
        gr.setValue('email', body.email);

        // Optional fields
        if (body.phone) gr.setValue('phone', body.phone);
        if (body.account_number) gr.setValue('account_number', body.account_number);
        if (body.address) {
          if (body.address.street) gr.setValue('street', body.address.street);
          if (body.address.city) gr.setValue('city', body.address.city);
          if (body.address.state) gr.setValue('state', body.address.state);
          if (body.address.zip) gr.setValue('zip', body.address.zip);
          if (body.address.country) gr.setValue('country', body.address.country);
        }

        gr.setValue('active', true);

        // Check create permission
        if (!gr.canCreate()) {
          response.setStatus(403);
          response.setBody({
            error: {
              message: 'Permission denied to create customer',
              code: 'FORBIDDEN'
            }
          });
          return;
        }

        var sysId = gr.insert();

        if (sysId) {
          // Set Location header for created resource
          response.setHeader('Location', request.url + '/' + sysId);
          response.setStatus(201);
          response.setBody({
            result: {
              sys_id: sysId,
              message: 'Customer created successfully'
            }
          });
        } else {
          response.setStatus(500);
          response.setBody({
            error: {
              message: 'Failed to create customer',
              code: 'INTERNAL_ERROR'
            }
          });
        }

      })(request, response);
    short_description: Create a new customer
    requires_acl_authorization: true
    requires_authentication: true
ステップ 3.4:PUT リソース(更新)

既存の顧客を更新するエンドポイントを作成します。

MCP を使用する場合:

Tool: SN-Create-Record
Parameters:
  table_name: sys_ws_operation
  data:
    name: Update Customer
    web_service_definition: [api_definition_sys_id]
    http_method: PUT
    relative_path: /customers/{id}
    operation_script: |
      (function process(/*RESTAPIRequest*/ request, /*RESTAPIResponse*/ response) {

        var customerId = request.pathParams.id;
        var body = request.body.data;

        // Find customer
        var gr = new GlideRecord('customer');
        if (!gr.get(customerId)) {
          response.setStatus(404);
          response.setBody({
            error: {
              message: 'Customer not found',
              code: 'NOT_FOUND'
            }
          });
          return;
        }

        // Check update permission
        if (!gr.canWrite()) {
          response.setStatus(403);
          response.setBody({
            error: {
              message: 'Permission denied to update customer',
              code: 'FORBIDDEN'
            }
          });
          return;
        }

        // Validate email if being changed
        if (body.email && body.email !== gr.getValue('email')) {
          if (!body.email.match(/^[^\s@]+@[^\s@]+\.[^\s@]+$/)) {
            response.setStatus(400);
            response.setBody({
              error: {
                message: 'Invalid email format',
                code: 'VALIDATION_ERROR'
              }
            });
            return;
          }

          // Check for duplicate
          var existing = new GlideRecord('customer');
          existing.addQuery('email', body.email);
          existing.addQuery('sys_id', '!=', customerId);
          existing.query();
          if (existing.next()) {
            response.setStatus(409);
            response.setBody({
              error: {
                message: 'Another customer with this email exists',
                code: 'DUPLICATE_ERROR'
              }
            });
            return;
          }
        }

        // Update fields
        var updatedFields = [];

        if (body.name !== undefined) {
          gr.setValue('name', body.name);
          updatedFields.push('name');
        }
        if (body.email !== undefined) {
          gr.setValue('email', body.email);
          updatedFields.push('email');
        }
        if (body.phone !== undefined) {
          gr.setValue('phone', body.phone);
          updatedFields.push('phone');
        }
        if (body.active !== undefined) {
          gr.setValue('active', body.active);
          updatedFields.push('active');
        }
        if (body.address) {
          if (body.address.street !== undefined) gr.setValue('street', body.address.street);
          if (body.address.city !== undefined) gr.setValue('city', body.address.city);
          if (body.address.state !== undefined) gr.setValue('state', body.address.state);
          if (body.address.zip !== undefined) gr.setValue('zip', body.address.zip);
          if (body.address.country !== undefined) gr.setValue('country', body.address.country);
          updatedFields.push('address');
        }

        gr.update();

        response.setStatus(200);
        response.setBody({
          result: {
            sys_id: customerId,
            message: 'Customer updated successfully',
            updated_fields: updatedFields
          }
        });

      })(request, response);
    short_description: Update an existing customer
    requires_acl_authorization: true
    requires_authentication: true
ステップ 3.5:DELETE リソース

顧客を削除するエンドポイントを作成します。

MCP を使用する場合:

Tool: SN-Create-Record
Parameters:
  table_name: sys_ws_operation
  data:
    name: Delete Customer
    web_service_definition: [api_definition_sys_id]
    http_method: DELETE
    relative_path: /customers/{id}
    operation_script: |
      (function process(/*RESTAPIRequest*/ request, /*RESTAPIResponse*/ response) {

        var customerId = request.pathParams.id;

        // Find customer
        var gr = new GlideRecord('customer');
        if (!gr.get(customerId)) {
          response.setStatus(404);
          response.setBody({
            error: {
              message: 'Customer not found',
              code: 'NOT_FOUND'
            }
          });
          return;
        }

        // Check delete permission
        if (!gr.canDelete()) {
          response.setStatus(403);
          response.setBody({
            error: {
              message: 'Permission denied to delete customer',
              code: 'FORBIDDEN'
            }
          });
          return;
        }

        // Soft delete vs hard delete decision
        // Option 1: Soft delete (preferred)
        gr.setValue('active', false);
        gr.update();

        // Option 2: Hard delete (use with caution)
        // gr.deleteRecord();

        response.setStatus(200);
        response.setBody({
          result: {
            sys_id: customerId,
            message: 'Customer deactivated successfully'
          }
        });

        // Alternative: Return 204 No Content for true DELETE
        // response.setStatus(204);

      })(request, response);
    short_description: Delete (deactivate) a customer
    requires_acl_authorization: true
    requires_authentication: true
フェーズ 4:認証とセキュリティ
ステップ 4.1:API レベルの認証を設定する

API 定義で認証要件を制御します。

Tool: SN-Update-Record
Parameters:
  table_name: sys_ws_definition
  sys_id: [api_definition_sys_id]
  data:
    requires_authentication: true
    enforce_acl: true
    acl_failure_result: UNAUTHORIZED
ステップ 4.2:API キー認証を実装する

シンプルな連携の場合は、API キーの検証を実装します。

API キーの Script Include を作成する:

Tool: SN-Create-Record
Parameters:
  table_name: sys_script_include
  data:
    name: CustomerAPIAuth
    script: |
      var CustomerAPIAuth = Class.create();
      CustomerAPIAuth.prototype = {
        initialize: function() {
          this.API_KEY_HEADER = 'X-API-Key';
          this.API_KEY_TABLE = 'x_company_api_keys';
        },

        validateApiKey: function(request) {
          var apiKey = request.getHeader(this.API_KEY_HEADER);

          if (!apiKey) {
            return {
              valid: false,
              error: 'API key required',
              code: 'MISSING_API_KEY'
            };
          }

          // Look up API key
          var keyRecord = new GlideRecord(this.API_KEY_TABLE);
          keyRecord.addQuery('key_value', apiKey);
          keyRecord.addQuery('active', true);
          keyRecord.addQuery('expires', '>', new GlideDateTime());
          keyRecord.query();

          if (!keyRecord.next()) {
            return {
              valid: false,
              error: 'Invalid or expired API key',
              code: 'INVALID_API_KEY'
            };
          }

          // Update last used timestamp
          keyRecord.setValue('last_used', new GlideDateTime());
          keyRecord.update();

          return {
            valid: true,
            client_id: keyRecord.getValue('client_id'),
            permissions: keyRecord.getValue('permissions').split(','),
            rate_limit: parseInt(keyRecord.getValue('rate_limit')) || 1000
          };
        },

        type: 'CustomerAPIAuth'
      };
    api_name: CustomerAPIAuth
    active: true
    access: public

API オペレーションで使用する:

(function process(/*RESTAPIRequest*/ request, /*RESTAPIResponse*/ response) {

  // Validate API key
  var auth = new CustomerAPIAuth();
  var authResult = auth.validateApiKey(request);

  if (!authResult.valid) {
    response.setStatus(401);
    response.setBody({
      error: {
        message: authResult.error,
        code: authResult.code
      }
    });
    return;
  }

  // Check permission
  if (authResult.permissions.indexOf('read:customers') === -1) {
    response.setStatus(403);
    response.setBody({
      error: {
        message: 'Insufficient permissions',
        code: 'FORBIDDEN',
        required: 'read:customers'
      }
    });
    return;
  }

  // Continue with request processing...

})(request, response);
ステップ 4.3:レート制限を実装する

API を保護するために、レート制限を追加します。

レートリミッターの Script Include を作成する:

Tool: SN-Create-Record
Parameters:
  table_name: sys_script_include
  data:
    name: APIRateLimiter
    script: |
      var APIRateLimiter = Class.create();
      APIRateLimiter.prototype = {
        initialize: function(clientId, rateLimit) {
          this.clientId = clientId;
          this.rateLimit = rateLimit || 100;  // requests per minute
          this.CACHE_PREFIX = 'api_rate_';
        },

        isAllowed: function() {
          var cacheKey = this.CACHE_PREFIX + this.clientId;
          var cache = new GlideSysCache('APIRateLimit');

          // Get current count
          var countStr = cache.get(cacheKey);
          var count = countStr ? parseInt(countStr) : 0;

          if (count >= this.rateLimit) {
            return {
              allowed: false,
              remaining: 0,
              reset_in: 60
            };
          }

          // Increment count with 60 second TTL
          cache.put(cacheKey, String(count + 1), 60);

          return {
            allowed: true,
            remaining: this.rateLimit - count - 1,
            limit: this.rateLimit
          };
        },

        type: 'APIRateLimiter'
      };
    api_name: APIRateLimiter
    active: true

API でレート制限を使用する:

// After authentication
var rateLimiter = new APIRateLimiter(authResult.client_id, authResult.rate_limit);
var rateResult = rateLimiter.isAllowed();

response.setHeader('X-RateLimit-Limit', rateResult.limit || authResult.rate_limit);
response.setHeader('X-RateLimit-Remaining', rateResult.remaining);

if (!rateResult.allowed) {
  response.setStatus(429);
  response.setHeader('Retry-After', rateResult.reset_in);
  response.setBody({
    error: {
      message: 'Rate limit exceeded',
      code: 'RATE_LIMITED',
      retry_after: rateResult.reset_in
    }
  });
  return;
}
フェーズ 5:クエリパラメーターとパスパラメーター
ステップ 5.1:一般的なクエリパラメーターのパターン
// Pagination
var limit = Math.min(parseInt(request.queryParams.limit) || 20, 100);
var offset = parseInt(request.queryParams.offset) || 0;
var page = parseInt(request.queryParams.page) || 1;
offset = (page - 1) * limit;  // If using page instead of offset

// Sorting
var sortField = request.queryParams.sort || 'name';
var sortOrder = request.queryParams.order || 'asc';
var validSortFields = ['name', 'email', 'created', 'updated'];
if (validSortFields.indexOf(sortField) === -1) {
  sortField = 'name';
}
if (sortOrder === 'desc') {
  gr.orderByDesc(sortField);
} else {
  gr.orderBy(sortField);
}

// Filtering
var filters = {
  active: request.queryParams.active,
  category: request.queryParams.category,
  created_after: request.queryParams.created_after,
  created_before: request.queryParams.created_before
};

if (filters.active !== undefined) {
  gr.addQuery('active', filters.active === 'true');
}
if (filters.category) {
  gr.addQuery('category', filters.category);
}
if (filters.created_after) {
  gr.addQuery('sys_created_on', '>=', filters.created_after);
}
if (filters.created_before) {
  gr.addQuery('sys_created_on', '<=', filters.created_before);
}

// Field selection
var fields = request.queryParams.fields;
if (fields) {
  var fieldList = fields.split(',');
  // Validate fields before use
  var validFields = ['sys_id', 'name', 'email', 'phone', 'active'];
  fieldList = fieldList.filter(function(f) {
    return validFields.indexOf(f.trim()) !== -1;
  });
}

// Search/Text filter
var searchQuery = request.queryParams.q || request.queryParams.search;
if (searchQuery) {
  var qc = gr.addQuery('name', 'CONTAINS', searchQuery);
  qc.addOrCondition('description', 'CONTAINS', searchQuery);
}
ステップ 5.2:パスパラメーターのパターン
// Single path parameter
// URL: /customers/{id}
var customerId = request.pathParams.id;

// Multiple path parameters
// URL: /customers/{customer_id}/orders/{order_id}
var customerId = request.pathParams.customer_id;
var orderId = request.pathParams.order_id;

// Validate path parameters
if (!customerId || customerId.length < 1) {
  response.setStatus(400);
  response.setBody({
    error: {
      message: 'Invalid customer ID',
      code: 'INVALID_PARAMETER'
    }
  });
  return;
}

// Support multiple ID formats
function resolveCustomerId(id) {
  // sys_id (32 char hex)
  if (/^[a-f0-9]{32}$/i.test(id)) {
    return { field: 'sys_id', value: id };
  }
  // Account number (numeric or alphanumeric)
  if (/^[A-Z]{2}\d{6}$/.test(id)) {
    return { field: 'account_number', value: id };
  }
  // Email
  if (id.indexOf('@') !== -1) {
    return { field: 'email', value: id };
  }
  return null;
}

var idInfo = resolveCustomerId(customerId);
if (!idInfo) {
  response.setStatus(400);
  response.setBody({
    error: {
      message: 'Unrecognized ID format',
      code: 'INVALID_ID_FORMAT',
      supported_formats: ['sys_id', 'account_number (XX000000)', 'email']
    }
  });
  return;
}
フェーズ 6:エラー処理とステータスコード
ステップ 6.1:標準的なエラーレスポンス形式
// Error response helper
function sendError(response, status, message, code, details) {
  response.setStatus(status);
  var error = {
    error: {
      message: message,
      code: code,
      timestamp: new GlideDateTime().getValue()
    }
  };
  if (details) {
    error.error.details = details;
  }
  response.setBody(error);
}

// Usage examples
sendError(response, 400, 'Invalid request body', 'BAD_REQUEST');
sendError(response, 401, 'Authentication required', 'UNAUTHORIZED');
sendError(response, 403, 'Permission denied', 'FORBIDDEN');
sendError(response, 404, 'Resource not found', 'NOT_FOUND');
sendError(response, 409, 'Conflict with existing resource', 'CONFLICT');
sendError(response, 422, 'Validation failed', 'VALIDATION_ERROR', errors);
sendError(response, 429, 'Rate limit exceeded', 'RATE_LIMITED');
sendError(response, 500, 'Internal server error', 'INTERNAL_ERROR');
sendError(response, 503, 'Service unavailable', 'SERVICE_UNAVAILABLE');
ステップ 6.2:包括的なエラー処理
(function process(/*RESTAPIRequest*/ request, /*RESTAPIResponse*/ response) {

  try {
    // Wrap entire request processing in try-catch

    // Validate content type for POST/PUT
    if (['POST', 'PUT', 'PATCH'].indexOf(request.getMethod()) !== -1) {
      var contentType = request.getHeader('Content-Type');
      if (!contentType || contentType.indexOf('application/json') === -1) {
        response.setStatus(415);
        response.setBody({
          error: {
            message: 'Content-Type must be application/json',
            code: 'UNSUPPORTED_MEDIA_TYPE'
          }
        });
        return;
      }
    }

    // Parse body with error handling
    var body;
    try {
      body = request.body.data;
      if (!body && request.body.dataString) {
        body = JSON.parse(request.body.dataString);
      }
    } catch (parseError) {
      response.setStatus(400);
      response.setBody({
        error: {
          message: 'Invalid JSON in request body',
          code: 'PARSE_ERROR',
          detail: parseError.message
        }
      });
      return;
    }

    // Main processing logic here...

  } catch (e) {
    // Log error for debugging
    gs.error('[CustomerAPI] Unhandled error: ' + e.message + '\nStack: ' + e.stack);

    response.setStatus(500);
    response.setBody({
      error: {
        message: 'An unexpected error occurred',
        code: 'INTERNAL_ERROR',
        reference: gs.generateGUID()  // For support reference
      }
    });
  }

})(request, response);
ステップ 6.3:HTTP ステータスコードのリファレンス
ステータス意味使用する状況
200OKGET、PUT、PATCH、DELETE の成功
201作成済み新しいリソースを作成する POST の成功
204コンテンツなしレスポンスボディを伴わない DELETE の成功
400不正なリクエスト構文が無効、または必須フィールドが不足
401認証されていない認証情報がない、または無効
403禁止認証済みだが、権限がない
404見つからないリソースが存在しない
405メソッドが許可されていないエンドポイントで HTTP メソッドがサポートされていない
409競合リソースの重複、または状態の競合
415サポートされていないメディアタイプContent-Type ヘッダーが正しくない
422処理できないエンティティ検証エラー
429リクエストが多すぎるレート制限を超過
500内部サーバーエラー予期しないサーバーエラー
503サービス利用不可一時的なメンテナンス/過負荷
フェーズ 7:API のバージョン管理
ステップ 7.1:バージョンの設定

バージョン管理用に API 定義を更新する:

Tool: SN-Update-Record
Parameters:
  table_name: sys_ws_definition
  sys_id: [api_definition_sys_id]
  data:
    is_versioned: true
    baseline_version: v1
    deprecated_versions: v1-beta
ステップ 7.2:バージョン固有のオペレーション

バージョン固有のエンドポイントを作成します。

Tool: SN-Create-Record
Parameters:
  table_name: sys_ws_operation
  data:
    name: List Customers v2
    web_service_definition: [api_definition_sys_id]
    http_method: GET
    relative_path: /v2/customers
    operation_script: |
      // V2 version with enhanced response format
      (function process(/*RESTAPIRequest*/ request, /*RESTAPIResponse*/ response) {

        // V2 includes additional fields and different structure
        var customers = [];
        var gr = new GlideRecord('customer');
        gr.query();

        while (gr.next()) {
          customers.push({
            id: gr.getUniqueValue(),
            attributes: {
              name: gr.getValue('name'),
              email: gr.getValue('email'),
              phone: gr.getValue('phone')
            },
            relationships: {
              contacts: '/api/x_company/customer_api/v2/customers/' + gr.getUniqueValue() + '/contacts',
              orders: '/api/x_company/customer_api/v2/customers/' + gr.getUniqueValue() + '/orders'
            },
            meta: {
              created: gr.getValue('sys_created_on'),
              updated: gr.getValue('sys_updated_on')
            }
          });
        }

        response.setStatus(200);
        response.setBody({
          data: customers,
          links: {
            self: request.url,
            next: null,
            prev: null
          }
        });

      })(request, response);
ステップ 7.3:非推奨化ヘッダー
// Add deprecation headers for old versions
if (request.url.indexOf('/v1/') !== -1) {
  response.setHeader('Deprecation', 'true');
  response.setHeader('Sunset', 'Sat, 01 Jul 2027 00:00:00 GMT');
  response.setHeader('Link', '</api/x_company/customer_api/v2/customers>; rel="successor-version"');
}
フェーズ 8:ドキュメントとテスト
ステップ 8.1:API Explorer との連携

ServiceNow は API Explorer のドキュメントを自動生成します。さらに充実させましょう。

Tool: SN-Update-Record
Parameters:
  table_name: sys_ws_definition
  sys_id: [api_definition_sys_id]
  data:
    doc_link: https://docs.company.com/api/customers
    short_description: |
      Customer management API for external integrations.

      Authentication: Basic Auth or API Key (X-API-Key header)
      Rate Limit: 100 requests per minute

      Base URL: https://instance.service-now.com/api/x_company/customer_api/v1
ステップ 8.2:curl によるテスト

GET リクエスト:

# List customers
curl -X GET \
  "https://dev12345.service-now.com/api/x_company/customer_api/v1/customers?limit=10&offset=0" \
  -H "Accept: application/json" \
  -H "Authorization: Basic $(echo -n 'username:password' | base64)"

# Get specific customer
curl -X GET \
  "https://dev12345.service-now.com/api/x_company/customer_api/v1/customers/abc123def456" \
  -H "Accept: application/json" \
  -H "X-API-Key: your-api-key-here"

POST リクエスト:

curl -X POST \
  "https://dev12345.service-now.com/api/x_company/customer_api/v1/customers" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -H "Authorization: Basic $(echo -n 'username:password' | base64)" \
  -d '{
    "name": "Acme Corporation",
    "email": "[email protected]",
    "phone": "555-1234",
    "address": {
      "street": "123 Main St",
      "city": "Springfield",
      "state": "IL",
      "zip": "62701"
    }
  }'

PUT リクエスト:

curl -X PUT \
  "https://dev12345.service-now.com/api/x_company/customer_api/v1/customers/abc123def456" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -H "Authorization: Basic $(echo -n 'username:password' | base64)" \
  -d '{
    "name": "Acme Corporation Updated",
    "phone": "555-5678"
  }'

DELETE リクエスト:

curl -X DELETE \
  "https://dev12345.service-now.com/api/x_company/customer_api/v1/customers/abc123def456" \
  -H "Accept: application/json" \
  -H "Authorization: Basic $(echo -n 'username:password' | base64)"
ステップ 8.3:Postman によるテスト

Postman コレクションの構造:

{
  "info": {
    "name": "Customer API",
    "description": "ServiceNow Customer Integration API"
  },
  "variable": [
    { "key": "base_url", "value": "https://dev12345.service-now.com" },
    { "key": "api_path", "value": "/api/x_company/customer_api/v1" }
  ],
  "auth": {
    "type": "basic",
    "basic": [
      { "key": "username", "value": "{{username}}" },
      { "key": "password", "value": "{{password}}" }
    ]
  },
  "item": [
    {
      "name": "List Customers",
      "request": {
        "method": "GET",
        "url": "{{base_url}}{{api_path}}/customers?limit=10"
      }
    },
    {
      "name": "Create Customer",
      "request": {
        "method": "POST",
        "url": "{{base_url}}{{api_path}}/customers",
        "body": {
          "mode": "raw",
          "raw": "{ \"name\": \"Test Customer\", \"email\": \"[email protected]\" }"
        }
      }
    }
  ]
}

Postman テスト:

// Test response status
pm.test("Status code is 200", function () {
  pm.response.to.have.status(200);
});

// Test response structure
pm.test("Response has result array", function () {
  var jsonData = pm.response.json();
  pm.expect(jsonData).to.have.property('result');
  pm.expect(jsonData.result).to.be.an('array');
});

// Test pagination
pm.test("Response has meta with pagination", function () {
  var jsonData = pm.response.json();
  pm.expect(jsonData.meta).to.have.property('total');
  pm.expect(jsonData.meta).to.have.property('limit');
  pm.expect(jsonData.meta).to.have.property('offset');
});

// Save sys_id for subsequent requests
pm.test("Save created customer ID", function () {
  var jsonData = pm.response.json();
  if (jsonData.result && jsonData.result.sys_id) {
    pm.environment.set("customer_id", jsonData.result.sys_id);
  }
});
フェーズ 9:公開と有効化
ステップ 9.1:公開前のチェックリスト

公開する前に、次の項目を確認します。

Tool: SN-Query-Table
Parameters:
  table_name: sys_ws_operation
  query: web_service_definition=[api_definition_sys_id]
  fields: name,http_method,relative_path,requires_authentication

チェックリスト:

  • すべてのオペレーションで認証が有効になっている
  • すべてのスクリプトにエラー処理が実装されている
  • レート制限が設定されている
  • 入力検証が完了している
  • ACL の強制が有効になっている
  • ドキュメントが更新されている
  • テストケースがすべて成功している
  • 更新セットが取得されている
ステップ 9.2:API を公開する
Tool: SN-Update-Record
Parameters:
  table_name: sys_ws_definition
  sys_id: [api_definition_sys_id]
  data:
    published: true
    active: true
ステップ 9.3:公開を確認する
Tool: SN-Query-Table
Parameters:
  table_name: sys_ws_definition
  query: sys_id=[api_definition_sys_id]
  fields: name,active,published,service_address

API は service_address URL でアクセスできるようになりました。

ツール使用の概要

オペレーションMCP ツール目的
API の作成SN-Create-Record (sys_ws_definition)API コンテナーを作成する
リソースの作成SN-Create-Record (sys_ws_operation)エンドポイントを追加する
API の更新SN-Update-Record設定を変更する
API のクエリSN-Query-TableAPI を一覧表示/確認する
スキーマの検出SN-Get-Table-Schemaテーブル構造を把握する
スクリプトのテストSN-Execute-Background-ScriptAPI ロジックをテストする

ベストプラクティス

  • 設計を優先: 実装前に API 構造を計画します
  • 最初からバージョン管理: 常にバージョン管理を有効にします(is_versioned: true)
  • 一貫したレスポンス: 標準的なエラー/成功フォーマットを使用します
  • すべてを検証: 処理前に入力を検証します
  • デフォルトでセキュアに: すべてのエンドポイントで認証を有効にします
  • 十分にドキュメント化: short_description と doc_link を使用します
  • 徹底的にテスト: 公開前に curl/Postman を使用します
  • 使用状況を監視: デバッグとメトリクスのために API 呼び出しをログに記録します
  • レート制限: 悪用を防ぎます
  • スクリプトインクルードを使用: 再利用可能なロジックを一元管理します

トラブルシューティング

API が 404 を返す

症状: API エンドポイントが見つかりません 原因:

  • API が公開されていない/有効になっていない
  • URL パスが正しくない
  • 名前空間が正しくない 解決策:
Tool: SN-Query-Table
Parameters:
  table_name: sys_ws_definition
  query: name=[your_api_name]
  fields: active,published,service_address,namespace
401 Unauthorized

症状: 認証に失敗します 原因:

  • 認証情報が無効
  • ユーザーに REST API へのアクセス権がない
  • web_service_admin ロールがない 解決策:
  1. まず Table API で認証情報を確認します
  2. ユーザーに rest_service ロールがあることを確認します
  3. カスタム認証を使用している場合は、API キーを確認します
403 Forbidden

症状: 認証後にアクセスが拒否されます 原因:

  • ACL によってアクセスがブロックされている
  • テーブル権限がない
  • スコープ付きアプリの制限 解決策:
  1. 基盤となるテーブルの ACL を確認します
  2. ユーザーロールが ACL の要件を満たしていることを確認します
  3. API の enforce_acl 設定を確認します
500 Internal Server Error

症状: 詳細のないサーバーエラー 原因:

  • オペレーション内のスクリプトエラー
  • 無効な GlideRecord クエリ
  • スクリプト内の null 参照 解決策:
  1. System Logs でスクリプトエラーを確認します
  2. オペレーションスクリプトに try-catch を追加します
  3. まずバックグラウンドスクリプトでスクリプトをテストします
レスポンスボディが空

症状: ステータスは 200 ですが、ボディがありません 原因:

  • response.setBody() がない
  • スクリプトが早期に終了した
  • return の前にボディが設定されている 解決策:
  1. response.setBody() が呼び出されていることを確認します
  2. すべてのコードパスでレスポンスが設定されることを確認します
  3. レスポンスを返さずに早期 return する箇所がないことを確認します
スクリプトが実行されない

症状: API はレスポンスを返しますが、スクリプトが実行されません 原因:

  • スクリプトの構文エラー
  • オペレーションが API にリンクされていない
  • HTTP メソッドが正しくない 解決策:
Tool: SN-Query-Table
Parameters:
  table_name: sys_ws_operation
  query: web_service_definition=[api_sys_id]
  fields: name,http_method,active,operation_script

例

例 1:最小限の CRUD API

基本的な CRUD を備えた最小限の API 一式:

# 1. Create API Definition
Tool: SN-Create-Record
Parameters:
  table_name: sys_ws_definition
  data:
    name: Simple Task API
    namespace: x_company
    short_description: Simple task management API
    active: true
    is_versioned: true
    baseline_version: v1

# 2. Create List Operation
Tool: SN-Create-Record
Parameters:
  table_name: sys_ws_operation
  data:
    name: List Tasks
    web_service_definition: [api_sys_id]
    http_method: GET
    relative_path: /tasks
    operation_script: |
      (function process(request, response) {
        var gr = new GlideRecord('x_company_task');
        gr.setLimit(100);
        gr.query();
        var tasks = [];
        while (gr.next()) {
          tasks.push({
            sys_id: gr.getUniqueValue(),
            name: gr.getValue('name'),
            status: gr.getValue('status')
          });
        }
        response.setBody({ result: tasks });
      })(request, response);

# 3. Create Get Operation
Tool: SN-Create-Record
Parameters:
  table_name: sys_ws_operation
  data:
    name: Get Task
    web_service_definition: [api_sys_id]
    http_method: GET
    relative_path: /tasks/{id}
    operation_script: |
      (function process(request, response) {
        var gr = new GlideRecord('x_company_task');
        if (gr.get(request.pathParams.id)) {
          response.setBody({
            result: {
              sys_id: gr.getUniqueValue(),
              name: gr.getValue('name'),
              description: gr.getValue('description'),
              status: gr.getValue('status')
            }
          });
        } else {
          response.setStatus(404);
          response.setBody({ error: { message: 'Task not found' }});
        }
      })(request, response);
例 2:集計を使用した検索 API
(function process(/*RESTAPIRequest*/ request, /*RESTAPIResponse*/ response) {

  var searchTerm = request.queryParams.q;
  var category = request.queryParams.category;
  var includeStats = request.queryParams.stats === 'true';

  // Search incidents
  var gr = new GlideRecord('incident');

  if (searchTerm) {
    var qc = gr.addQuery('short_description', 'CONTAINS', searchTerm);
    qc.addOrCondition('description', 'CONTAINS', searchTerm);
    qc.addOrCondition('number', searchTerm);
  }

  if (category) {
    gr.addQuery('category', category);
  }

  gr.addQuery('active', true);
  gr.setLimit(50);
  gr.orderByDesc('sys_created_on');
  gr.query();

  var results = [];
  while (gr.next()) {
    results.push({
      number: gr.getValue('number'),
      short_description: gr.getValue('short_description'),
      priority: gr.getDisplayValue('priority'),
      state: gr.getDisplayValue('state'),
      created: gr.getValue('sys_created_on')
    });
  }

  var responseBody = {
    query: searchTerm,
    count: results.length,
    results: results
  };

  // Add statistics if requested
  if (includeStats) {
    var stats = {};

    // Count by priority
    var priorityAgg = new GlideAggregate('incident');
    priorityAgg.addQuery('active', true);
    priorityAgg.addAggregate('COUNT');
    priorityAgg.groupBy('priority');
    priorityAgg.query();

    stats.by_priority = {};
    while (priorityAgg.next()) {
      var priority = priorityAgg.getDisplayValue('priority');
      stats.by_priority[priority] = parseInt(priorityAgg.getAggregate('COUNT'));
    }

    responseBody.stats = stats;
  }

  response.setStatus(200);
  response.setBody(responseBody);

})(request, response);
例 3:一括操作 API
(function process(/*RESTAPIRequest*/ request, /*RESTAPIResponse*/ response) {

  var body = request.body.data;

  // Validate bulk request
  if (!body.operations || !Array.isArray(body.operations)) {
    response.setStatus(400);
    response.setBody({
      error: {
        message: 'operations array required',
        code: 'VALIDATION_ERROR'
      }
    });
    return;
  }

  if (body.operations.length > 100) {
    response.setStatus(400);
    response.setBody({
      error: {
        message: 'Maximum 100 operations per request',
        code: 'LIMIT_EXCEEDED'
      }
    });
    return;
  }

  var results = [];

  body.operations.forEach(function(op, index) {
    var result = { index: index, operation: op.action };

    try {
      switch (op.action) {
        case 'create':
          var gr = new GlideRecord('customer');
          gr.initialize();
          for (var field in op.data) {
            gr.setValue(field, op.data[field]);
          }
          result.sys_id = gr.insert();
          result.success = !!result.sys_id;
          break;

        case 'update':
          var grUpdate = new GlideRecord('customer');
          if (grUpdate.get(op.sys_id)) {
            for (var updateField in op.data) {
              grUpdate.setValue(updateField, op.data[updateField]);
            }
            grUpdate.update();
            result.success = true;
          } else {
            result.success = false;
            result.error = 'Record not found';
          }
          break;

        case 'delete':
          var grDelete = new GlideRecord('customer');
          if (grDelete.get(op.sys_id)) {
            grDelete.setValue('active', false);
            grDelete.update();
            result.success = true;
          } else {
            result.success = false;
            result.error = 'Record not found';
          }
          break;

        default:
          result.success = false;
          result.error = 'Unknown action: ' + op.action;
      }
    } catch (e) {
      result.success = false;
      result.error = e.message;
    }

    results.push(result);
  });

  var successCount = results.filter(function(r) { return r.success; }).length;

  response.setStatus(successCount === results.length ? 200 : 207);  // 207 = Multi-Status
  response.setBody({
    summary: {
      total: results.length,
      succeeded: successCount,
      failed: results.length - successCount
    },
    results: results
  });

})(request, response);

関連スキル

  • admin/update-set-management - 更新セットに API 定義を取り込む
  • admin/application-scope - スコープ付きアプリケーションで API を作成する
  • security/acl-management - API セキュリティを設定する
  • admin/script-execution - API スクリプトをテストする
  • catalog/request-fulfillment - API 駆動のリクエスト処理

参考資料

コメント