mcpField
D
document-management

document-management

github.com/serac-labsドキュメント77105 回インストール9 週間前に更新
リポジトリ

document-management について

document-managementはServiceNowの文書を扱うスキルで、添付ファイル、テンプレート、文書生成、承認を扱います。スクリプト例に加え、添付ファイルのアップロード、ダウンロード、一覧表示、削除に使うツールを説明します。

主な機能

  1. 01レコードの添付ファイル管理
  2. 02テンプレート作成と文書生成
  3. 03HTMLからPDFを生成
  4. 04文書の承認とバージョン管理
  5. 05添付ファイルのアクセス権と制限を確認

活用シーン

  1. 01ServiceNowレコードへのファイル添付や取得
  2. 02テンプレートから文書やPDFを生成
  3. 03文書の承認とバージョン更新を管理

MCP 対応

添付ファイルのアップロード、ダウンロード、一覧表示、削除に加え、テーブル照会やスクリプト実行など、MCPクライアント向けのServiceNowツールを説明します。

インストール

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

$npx skills add https://github.com/serac-labs/serac --skill document-management

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

詳細

ServiceNow のドキュメント管理

ドキュメント管理では、添付ファイル、テンプレート、ドキュメントの生成を扱います。

ドキュメントアーキテクチャ

Record
    ├── Attachments (sys_attachment)
    │   └── Attachment Data (sys_attachment_doc)
    ├── Generated Documents
    └── Document Templates

主要なテーブル

テーブル用途
sys_attachment添付ファイルのメタデータ
sys_attachment_doc添付ファイルの内容
sys_report_templateレポートテンプレート
dms_documentドキュメントレコード

添付ファイル(ES5)

添付ファイルをアップロードする
// Attach file to record (ES5 ONLY!)
function attachFile(tableName, recordSysId, fileName, contentType, content) {
  var attachment = new GlideSysAttachment()

  // Content can be base64 encoded string
  var attachmentSysId = attachment.write(tableName, recordSysId, fileName, contentType, content)

  return attachmentSysId
}

// Example
var base64Content = "SGVsbG8gV29ybGQh" // Base64 encoded
attachFile("incident", incidentSysId, "notes.txt", "text/plain", base64Content)
添付ファイルを読み取る
// Read attachment content (ES5 ONLY!)
function getAttachmentContent(attachmentSysId) {
  var attachment = new GlideSysAttachment()
  var content = attachment.getContent(attachmentSysId)
  return content
}

// Get attachment as base64
function getAttachmentBase64(attachmentSysId) {
  var attachment = new GlideSysAttachment()
  var bytes = attachment.getBytes(attachmentSysId)

  // Convert to base64
  var base64 = GlideBase64.encode(bytes)
  return base64
}
添付ファイルを一覧表示する
// Get all attachments for record (ES5 ONLY!)
function getRecordAttachments(tableName, recordSysId) {
  var attachments = []

  var gr = new GlideRecord("sys_attachment")
  gr.addQuery("table_name", tableName)
  gr.addQuery("table_sys_id", recordSysId)
  gr.query()

  while (gr.next()) {
    attachments.push({
      sys_id: gr.getUniqueValue(),
      file_name: gr.getValue("file_name"),
      content_type: gr.getValue("content_type"),
      size_bytes: gr.getValue("size_bytes"),
      created_on: gr.getValue("sys_created_on"),
      created_by: gr.sys_created_by.getDisplayValue(),
    })
  }

  return attachments
}
添付ファイルをコピーする
// Copy attachments between records (ES5 ONLY!)
function copyAttachments(sourceTable, sourceSysId, targetTable, targetSysId) {
  var attachment = new GlideSysAttachment()
  attachment.copy(sourceTable, sourceSysId, targetTable, targetSysId)
}

// Copy specific attachment
function copyOneAttachment(attachmentSysId, targetTable, targetSysId) {
  var source = new GlideRecord("sys_attachment")
  if (!source.get(attachmentSysId)) {
    return null
  }

  var attachment = new GlideSysAttachment()
  var content = attachment.getBytes(attachmentSysId)

  return attachment.write(
    targetTable,
    targetSysId,
    source.getValue("file_name"),
    source.getValue("content_type"),
    content,
  )
}

ドキュメントテンプレート(ES5)

ドキュメントテンプレートを作成する
// Create document template (ES5 ONLY!)
var template = new GlideRecord("sys_report_template")
template.initialize()

template.setValue("name", "Incident Summary Report")
template.setValue("table", "incident")
template.setValue("description", "Summary report for incident resolution")

// Template content with placeholders
template.setValue(
  "template",
  "<html>\n" +
    "<head><title>Incident Summary</title></head>\n" +
    "<body>\n" +
    "<h1>Incident Summary Report</h1>\n" +
    "<h2>${number}</h2>\n" +
    "<p><strong>Description:</strong> ${short_description}</p>\n" +
    "<p><strong>Caller:</strong> ${caller_id.name}</p>\n" +
    "<p><strong>Priority:</strong> ${priority}</p>\n" +
    "<p><strong>State:</strong> ${state}</p>\n" +
    "<p><strong>Resolution:</strong> ${close_notes}</p>\n" +
    "<p><strong>Resolved By:</strong> ${resolved_by.name}</p>\n" +
    "<p><strong>Resolved On:</strong> ${resolved_at}</p>\n" +
    "</body>\n" +
    "</html>",
)

template.insert()
テンプレートからドキュメントを生成する
// Generate document from template (ES5 ONLY!)
function generateDocumentFromTemplate(templateName, recordSysId) {
  // Get template
  var template = new GlideRecord("sys_report_template")
  if (!template.get("name", templateName)) {
    return null
  }

  // Get record
  var tableName = template.getValue("table")
  var gr = new GlideRecord(tableName)
  if (!gr.get(recordSysId)) {
    return null
  }

  // Process template
  var content = template.getValue("template")
  content = processTemplateVariables(content, gr)

  return content
}

function processTemplateVariables(template, gr) {
  // Replace ${field} and ${field.property} patterns
  var pattern = /\$\{([^}]+)\}/g
  var match

  while ((match = pattern.exec(template)) !== null) {
    var fieldPath = match[1]
    var value = getFieldValue(gr, fieldPath)
    template = template.replace(match[0], value)
  }

  return template
}

function getFieldValue(gr, fieldPath) {
  var parts = fieldPath.split(".")
  var value = gr

  for (var i = 0; i < parts.length; i++) {
    if (!value) return ""

    if (i < parts.length - 1) {
      // Reference field - get referenced record
      value = value[parts[i]].getRefRecord()
    } else {
      // Final field
      if (value.isValidField && value.isValidField(parts[i])) {
        value = value[parts[i]].getDisplayValue() || value.getValue(parts[i])
      } else {
        value = ""
      }
    }
  }

  return value || ""
}

PDF の生成(ES5)

PDF を生成する
// Generate PDF from HTML (ES5 ONLY!)
function generatePDF(htmlContent, fileName) {
  try {
    var pdfCreator = new sn_pdfgeneratorutils.PDFGenerationAPI()
    var pdfBytes = pdfCreator.convertToPDF(htmlContent)

    // Return as base64
    return {
      success: true,
      content: GlideBase64.encode(pdfBytes),
      fileName: fileName || "document.pdf",
    }
  } catch (e) {
    gs.error("PDF generation failed: " + e.message)
    return {
      success: false,
      error: e.message,
    }
  }
}

// Generate and attach PDF to record
function generateAndAttachPDF(templateName, recordSysId, fileName) {
  // Generate HTML from template
  var html = generateDocumentFromTemplate(templateName, recordSysId)
  if (!html) {
    return { success: false, error: "Template generation failed" }
  }

  // Convert to PDF
  var pdf = generatePDF(html, fileName)
  if (!pdf.success) {
    return pdf
  }

  // Get table name from template
  var template = new GlideRecord("sys_report_template")
  template.get("name", templateName)
  var tableName = template.getValue("table")

  // Attach PDF
  var attachmentSysId = attachFile(tableName, recordSysId, fileName, "application/pdf", pdf.content)

  return {
    success: true,
    attachment_sys_id: attachmentSysId,
  }
}

ドキュメントワークフロー(ES5)

ドキュメントの承認
// Create document for approval (ES5 ONLY!)
function submitDocumentForApproval(documentSysId, approvers) {
  var doc = new GlideRecord("dms_document")
  if (!doc.get(documentSysId)) {
    return { success: false, message: "Document not found" }
  }

  // Update state
  doc.setValue("state", "pending_approval")
  doc.update()

  // Create approval records
  for (var i = 0; i < approvers.length; i++) {
    var approval = new GlideRecord("sysapproval_approver")
    approval.initialize()
    approval.setValue("sysapproval", documentSysId)
    approval.setValue("approver", approvers[i])
    approval.setValue("state", "requested")
    approval.insert()
  }

  // Notify approvers
  gs.eventQueue("document.approval.requested", doc, approvers.join(","), "")

  return { success: true }
}
バージョン管理
// Create new document version (ES5 ONLY!)
function createDocumentVersion(documentSysId, newContent, changeNotes) {
  var doc = new GlideRecord("dms_document")
  if (!doc.get(documentSysId)) {
    return null
  }

  // Get current version
  var currentVersion = parseInt(doc.getValue("version"), 10) || 1
  var newVersion = currentVersion + 1

  // Archive current version
  var archive = new GlideRecord("dms_document_version")
  archive.initialize()
  archive.setValue("document", documentSysId)
  archive.setValue("version", currentVersion)
  archive.setValue("content", doc.getValue("content"))
  archive.setValue("created_by", gs.getUserID())
  archive.insert()

  // Update document with new version
  doc.setValue("version", newVersion)
  doc.setValue("content", newContent)
  doc.work_notes = "Version " + newVersion + ": " + changeNotes
  doc.update()

  return {
    document_sys_id: documentSysId,
    version: newVersion,
  }
}

// Get document version history
function getDocumentVersions(documentSysId) {
  var versions = []

  var version = new GlideRecord("dms_document_version")
  version.addQuery("document", documentSysId)
  version.orderByDesc("version")
  version.query()

  while (version.next()) {
    versions.push({
      version: version.getValue("version"),
      created_on: version.getValue("sys_created_on"),
      created_by: version.sys_created_by.getDisplayValue(),
    })
  }

  return versions
}

添付ファイルのセキュリティ(ES5)

添付ファイルへのアクセスを確認する
// Check if user can access attachment (ES5 ONLY!)
function canAccessAttachment(attachmentSysId) {
  var attachment = new GlideRecord("sys_attachment")
  if (!attachment.get(attachmentSysId)) {
    return false
  }

  // Check access to parent record
  var parentTable = attachment.getValue("table_name")
  var parentSysId = attachment.getValue("table_sys_id")

  var parent = new GlideRecord(parentTable)
  return parent.get(parentSysId) // Returns false if no read access
}

ファイルの出し入れ:添付ファイルツール

この行より上はすべてスクリプト側の機能です。あるレコードから別のレコードへ添付ファイルをコピーしたり、 すでに添付されているファイルを読み取ったりできます。一方で、通常求められる次の2つの操作はどちらもできません。**エージェントが持っているファイルをレコードに取り込むこと、またはインスタンスからファイルを取り出してエージェントに渡すことです。**バックグラウンドスクリプトからエージェントのファイルシステムにはアクセスできず、数メガバイトものバイト列をエージェントに返すこともできません。代わりに、6つのツールが /api/now/attachment 経由で処理します。

ツール方向受け取るもの
snow_upload_attachment入力content 内の base64
snow_file_upload入力file_data 内の base64。同じエンドポイントで、パラメーター名が異なります
snow_attach_file入力ローカルパス。読み取って multipart で送信します
snow_get_attachments出力table_name + table_sys_id。メタデータのみを返します
snow_file_download出力attachment_sys_id。base64 とコンテンツタイプを返します
snow_delete_attachment—attachment_sys_id。完全に削除します
base64 またはパス

snow_upload_attachment と snow_file_upload は同じエンドポイントを同じセマンティクスで呼び出します。違いはパラメーター名(content/table_sys_id と file_data/record_sys_id)だけです。どちらもファイルを読み取りません。すでに手元にあるバイト列を渡します。

snow_attach_file はディスクからファイルを読み取るツールです。最初にファイルのサイズを確認し、max_size_mb を超える場合は拒否します。この値のデフォルトは25で、インスタンス側の制限ではなく、クライアント側のガードです。インスタンスには com.glide.attachment.max_size(MB、デフォルトは1024ですが、実際にはもっと低く設定されていることがよくあります)による独自の上限があり、さらに glide.attachment.extensions により、受け付ける拡張子が制限されます。25 MB 以内でもインスタンスの上限を超えるファイルは、ガードではなく API で 400 エラーになります。

base64 にするとサイズが3分の1増えるため、24 MB のファイルはリクエストボディが32 MBになります。大きなファイルを送信する場合は、snow_attach_file を優先してください。multipart ではバイト列がそのまま送信されます。

ファイルを読み戻す

snow_get_attachments が返すのはメタデータのみです。sys_id、file_name、size_bytes、content_type が含まれます。ファイルの内容を取得するには、必ず2回呼び出します。

const list = await snow_get_attachments({ table_name: "incident", table_sys_id: incSysId })
const shot = list.attachments.find((a) => a.content_type.indexOf("image/") === 0)
const file = await snow_file_download({ attachment_sys_id: shot.sys_id })  // base64 + content_type

レコードのすべての添付ファイルをまとめてダウンロードする呼び出しはありません。リストを自分でループ処理し、処理前に size_bytes を確認してください。ツールの結果にはファイル全体が base64 で含まれます。

テーブルで添付ファイルが制限されている場合

添付ファイルへのアクセスは、親レコードの ACL では決まりません。sys_attachment を通じて解決され、独立した 3 つの条件によって制限されます。

  • glide.attachment.role — 添付に必要なロールを指定するインスタンスプロパティです。
  • テーブルレベルの添付ファイル制御。 テーブルでは、名前付きロールを持つユーザーのみが添付または読み取りできるように設定できます。通常は、親テーブルを条件とする sys_attachment 上の sys_security_acl レコードで制御します。
  • 親レコード自体の読み取り ACL。 インシデントの読み取り権限を失うと、添付ファイルにもアクセスできなくなります。

この問題の現れ方は誤解を招くため、把握しておくことが重要です。sys_attachment はほかのテーブルと同じなので、読み取り権限のない呼び出し元にはエラーではなく空のリストが返されます。UI では添付ファイルが表示されているレコードに対して snow_get_attachments が [] を返す場合、レコードが空なのではなく、権限に問題があるということです。単一の添付ファイルの読み取りに失敗した場合は、アクセスできない sys_id の存在をプラットフォームが確認しないため、アクセス拒否ではなく未検出として表示されることがよくあります。そのため、同じレコードの一覧取得に成功した直後に「添付ファイルが見つかりません」と表示されても、ファイルが削除されたのではなく、2 回の読み取りで権限に違いがあります。ファイルがなくなったと判断する前に、sys_attachment を対象に snow_acl_explain で確認してください。

snow_delete_attachment は、sys_attachment のレコードと sys_attachment_doc のチャンクを完全に削除します。ごみ箱はなく、更新セットにも記録されません。これは設定ではなくデータです。

MCP ツールの統合

利用可能なツール
ツール目的
snow_query_table添付ファイルとテンプレートをクエリする
snow_execute_scriptドキュメントスクリプトをテストする
snow_artifact_manageアーティファクトを検索する(action: "find"、type は必須)
snow_get_attachmentsレコードに添付されているファイルを一覧表示する
snow_file_download1 つの添付ファイルのバイトデータを取得する
snow_attach_fileローカルファイルを添付する(マルチパート)
snow_upload_attachmentBase64 コンテンツを添付する
snow_file_uploadBase64 コンテンツを添付する(別のパラメーター名を使用)
snow_delete_attachment添付ファイルを完全に削除する
ワークフローの例
// 1. Get record attachments
await snow_query_table({
  table: "sys_attachment",
  query: "table_name=incident^table_sys_id=inc_sys_id",
  fields: "file_name,content_type,size_bytes,sys_created_on",
})

// 2. Generate document
await snow_execute_script({
  script: `
        var html = generateDocumentFromTemplate('Incident Summary', 'inc_sys_id');
        gs.info('Generated: ' + (html ? 'success' : 'failed'));
    `,
})

// 3. Find document templates
await snow_query_table({
  table: "sys_report_template",
  query: "table=incident",
  fields: "name,description,table",
})

ベストプラクティス

  1. ファイルの種類 - 許可する種類を制限します
  2. サイズ制限 - 最大ファイルサイズを設定します
  3. ウイルススキャン - アップロードをスキャンします
  4. アクセス制御 - 親レコードから継承します
  5. バージョン管理 - 変更を追跡します
  6. クリーンアップ - 孤立したファイルを削除します
  7. テンプレート - 一貫性を保つために使用します
  8. ES5 のみ - 最新の JavaScript 構文は使用しません

コメント