feat(jsm): add Atlassian Assets (Insight/CMDB) tools for asset management (#5072)

* feat(jsm): add Atlassian Assets (Insight/CMDB) tools for asset management

Add nine JSM Assets tools so workflows can read and write Atlassian Assets
(Insight/CMDB) objects — the foundation for keeping JSM asset tables in sync
for software/hardware asset management.

Tools (wired into the Jira Service Management block):
- jsm_list_object_schemas, jsm_get_object_schema
- jsm_list_object_types, jsm_get_object_type_attributes
- jsm_search_objects_aql (AQL search with pagination)
- jsm_get_object, jsm_create_object, jsm_update_object, jsm_delete_object

Each tool proxies through an internal route that resolves the Jira cloudId and
the Assets workspaceId, then calls the Assets API via the OAuth 2.0 (3LO)
gateway form (/ex/jira/{cloudId}/jsm/assets/workspace/{workspaceId}/v1).

Adds the CMDB OAuth scopes to the jira provider (read/write/delete cmdb-object,
read cmdb-schema/type/attribute) with descriptions, contract schemas for each
route, and block operations/subBlocks/outputs. Bumps the API-validation route
baseline for the nine new routes.

* refactor(jsm): harden Assets param coercion and response typing

- Add toOptionalInt helper so non-numeric pagination inputs never emit NaN
  into the Assets query string (startAt/maxResults/page/resultsPerPage)
- Replace Record<string, any> in mapAssetObject with typed Raw* interfaces

* fix(jsm): validate Assets workspaceId and honor `last` pagination flag

Address review findings on the Assets tools:
- Add validateAssetsWorkspaceId and guard the workspaceId in every Assets
  route before it is interpolated into the API path (mirrors the existing
  cloudId guard) — prevents a crafted workspaceId from escaping the
  workspace-scoped path
- Object schema list now falls back to the `last` flag when `isLast` is
  absent, so pagination doesn't stop early

* feat(jsm): allow overriding the auto-resolved Assets workspace

Atlassian provisions one Assets workspace per site, so workspace discovery
uses values[0] by design. For the rare multi-workspace site, expose an
advanced "Assets Workspace ID" override on the block that flows through to
every Assets operation, and document the single-workspace assumption.

* refactor(jsm): include Assets responses in the JsmResponse union

Append the nine Assets tool response types to JsmResponse for completeness
and consistency with the rest of the JSM tool surface.
This commit is contained in:
Waleed
2026-06-15 16:40:24 -07:00
committed by GitHub
parent d538b76eda
commit dd32abef1e
30 changed files with 3045 additions and 3 deletions
@@ -988,6 +988,272 @@ Copy forms from one Jira issue to another
| `copiedForms` | json | Array of successfully copied forms |
| `errors` | json | Array of errors encountered during copy |
### `jsm_list_object_schemas`
List Assets (Insight/CMDB) object schemas in Jira Service Management
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) |
| `cloudId` | string | No | Jira Cloud ID for the instance |
| `workspaceId` | string | No | Assets workspace ID \(resolved automatically when omitted\) |
| `startAt` | number | No | Pagination start index \(e.g., 0, 50\) |
| `maxResults` | number | No | Maximum schemas to return \(e.g., 25, 50\) |
| `includeCounts` | boolean | No | Include object and object-type counts per schema |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `ts` | string | Timestamp of the operation |
| `schemas` | array | List of Assets object schemas |
| ↳ `id` | string | Schema ID |
| ↳ `name` | string | Schema name |
| ↳ `objectSchemaKey` | string | Schema key |
| ↳ `status` | string | Schema status |
| ↳ `description` | string | Schema description |
| ↳ `objectCount` | number | Number of objects |
| ↳ `objectTypeCount` | number | Number of object types |
| `total` | number | Total number of schemas |
| `isLast` | boolean | Whether this is the last page |
### `jsm_get_object_schema`
Get a single Assets (Insight/CMDB) object schema by ID
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) |
| `cloudId` | string | No | Jira Cloud ID for the instance |
| `workspaceId` | string | No | Assets workspace ID \(resolved automatically when omitted\) |
| `schemaId` | string | Yes | The Assets object schema ID |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `ts` | string | Timestamp of the operation |
| `schema` | json | The Assets object schema |
| ↳ `id` | string | Schema ID |
| ↳ `name` | string | Schema name |
| ↳ `objectSchemaKey` | string | Schema key |
| ↳ `status` | string | Schema status |
| ↳ `description` | string | Schema description |
| ↳ `objectCount` | number | Number of objects |
| ↳ `objectTypeCount` | number | Number of object types |
### `jsm_list_object_types`
List object types within an Assets (Insight/CMDB) object schema
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) |
| `cloudId` | string | No | Jira Cloud ID for the instance |
| `workspaceId` | string | No | Assets workspace ID \(resolved automatically when omitted\) |
| `schemaId` | string | Yes | The Assets object schema ID to list object types for |
| `excludeAbstract` | boolean | No | Exclude abstract object types from the result |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `ts` | string | Timestamp of the operation |
| `objectTypes` | array | List of object types in the schema |
| ↳ `id` | string | Object type ID |
| ↳ `name` | string | Object type name |
| ↳ `description` | string | Object type description |
| ↳ `objectSchemaId` | string | Parent schema ID |
| ↳ `objectCount` | number | Number of objects |
| ↳ `abstractObjectType` | boolean | Whether the type is abstract |
| ↳ `inherited` | boolean | Whether the type inherits attributes |
| `total` | number | Total number of object types |
### `jsm_get_object_type_attributes`
Get the attribute definitions for an Assets (Insight/CMDB) object type. Use the returned attribute IDs to build create/update payloads or map columns.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) |
| `cloudId` | string | No | Jira Cloud ID for the instance |
| `workspaceId` | string | No | Assets workspace ID \(resolved automatically when omitted\) |
| `objectTypeId` | string | Yes | The Assets object type ID |
| `onlyValueEditable` | boolean | No | Return only attributes whose values can be edited |
| `query` | string | No | Filter attributes by a search query |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `ts` | string | Timestamp of the operation |
| `attributes` | array | Attribute definitions for the object type |
| ↳ `id` | string | Attribute definition ID — use as objectTypeAttributeId in create/update |
| ↳ `name` | string | Attribute name |
| ↳ `label` | boolean | Whether this attribute is the object label |
| ↳ `type` | number | Data type discriminator \(integer enum\) |
| ↳ `defaultType` | json | Default data type \{ id, name \} |
| ↳ `editable` | boolean | Whether the value is editable |
| ↳ `minimumCardinality` | number | Minimum number of values \(&gt;= 1 means required\) |
| ↳ `maximumCardinality` | number | Maximum number of values |
| ↳ `uniqueAttribute` | boolean | Whether values must be unique |
| `total` | number | Total number of attributes |
### `jsm_search_objects_aql`
Search Assets (Insight/CMDB) objects using AQL (Assets Query Language), e.g. objectType =
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) |
| `cloudId` | string | No | Jira Cloud ID for the instance |
| `workspaceId` | string | No | Assets workspace ID \(resolved automatically when omitted\) |
| `qlQuery` | string | Yes | AQL query string \(e.g., objectType = "Host" AND "Operating System" = "Ubuntu"\) |
| `page` | number | No | Page number \(1-based, defaults to 1\) |
| `resultsPerPage` | number | No | Results per page \(e.g., 25, 50\) |
| `includeAttributes` | boolean | No | Include resolved attribute values on each object \(defaults to true\) |
| `objectTypeId` | string | No | Optionally scope the search to a single object type ID |
| `objectSchemaId` | string | No | Optionally scope the search to a single object schema ID |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `ts` | string | Timestamp of the operation |
| `objects` | array | Matching Assets objects |
| ↳ `id` | string | Object ID |
| ↳ `label` | string | Object label |
| ↳ `objectKey` | string | Object key \(e.g., HOST-123\) |
| ↳ `objectType` | json | Object type metadata |
| ↳ `attributes` | json | Resolved attribute values |
| `total` | number | Total number of matching objects \(totalFilterCount\) |
| `pageNumber` | number | Current page number |
| `pageSize` | number | Number of objects on this page |
### `jsm_get_object`
Get a single Assets (Insight/CMDB) object by ID, including its attribute values
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) |
| `cloudId` | string | No | Jira Cloud ID for the instance |
| `workspaceId` | string | No | Assets workspace ID \(resolved automatically when omitted\) |
| `objectId` | string | Yes | The Assets object ID |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `ts` | string | Timestamp of the operation |
| `object` | json | The Assets object |
| ↳ `id` | string | Object ID |
| ↳ `label` | string | Human-readable object label |
| ↳ `objectKey` | string | Object key \(e.g., HOST-123\) |
| ↳ `globalId` | string | Global object ID |
| ↳ `objectType` | json | Object type metadata |
| ↳ `attributes` | json | Resolved attribute values for the object |
| ↳ `hasAvatar` | boolean | Whether the object has an avatar |
| ↳ `created` | string | Creation timestamp |
| ↳ `updated` | string | Last update timestamp |
| ↳ `link` | string | Self link to the object |
### `jsm_create_object`
Create an Assets (Insight/CMDB) object of a given object type. Attributes use objectTypeAttributeId values from the object type definition.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) |
| `cloudId` | string | No | Jira Cloud ID for the instance |
| `workspaceId` | string | No | Assets workspace ID \(resolved automatically when omitted\) |
| `objectTypeId` | string | Yes | The object type ID to create the object under |
| `attributes` | json | Yes | Array of attributes: \[\{ objectTypeAttributeId, objectAttributeValues: \[\{ value \}\] \}\] |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `ts` | string | Timestamp of the operation |
| `object` | json | The created Assets object |
| ↳ `id` | string | Object ID |
| ↳ `label` | string | Human-readable object label |
| ↳ `objectKey` | string | Object key \(e.g., HOST-123\) |
| ↳ `globalId` | string | Global object ID |
| ↳ `objectType` | json | Object type metadata |
| ↳ `attributes` | json | Resolved attribute values for the object |
| ↳ `hasAvatar` | boolean | Whether the object has an avatar |
| ↳ `created` | string | Creation timestamp |
| ↳ `updated` | string | Last update timestamp |
| ↳ `link` | string | Self link to the object |
### `jsm_update_object`
Update an existing Assets (Insight/CMDB) object. Provide the attributes to change using their objectTypeAttributeId values.
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) |
| `cloudId` | string | No | Jira Cloud ID for the instance |
| `workspaceId` | string | No | Assets workspace ID \(resolved automatically when omitted\) |
| `objectId` | string | Yes | The Assets object ID to update |
| `attributes` | json | Yes | Array of attributes to set: \[\{ objectTypeAttributeId, objectAttributeValues: \[\{ value \}\] \}\] |
| `objectTypeId` | string | No | Optional object type ID \(only if changing the type\) |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `ts` | string | Timestamp of the operation |
| `object` | json | The updated Assets object |
| ↳ `id` | string | Object ID |
| ↳ `label` | string | Human-readable object label |
| ↳ `objectKey` | string | Object key \(e.g., HOST-123\) |
| ↳ `globalId` | string | Global object ID |
| ↳ `objectType` | json | Object type metadata |
| ↳ `attributes` | json | Resolved attribute values for the object |
| ↳ `hasAvatar` | boolean | Whether the object has an avatar |
| ↳ `created` | string | Creation timestamp |
| ↳ `updated` | string | Last update timestamp |
| ↳ `link` | string | Self link to the object |
### `jsm_delete_object`
Delete an Assets (Insight/CMDB) object by ID
#### Input
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `domain` | string | Yes | Your Jira domain \(e.g., yourcompany.atlassian.net\) |
| `cloudId` | string | No | Jira Cloud ID for the instance |
| `workspaceId` | string | No | Assets workspace ID \(resolved automatically when omitted\) |
| `objectId` | string | Yes | The Assets object ID to delete |
#### Output
| Parameter | Type | Description |
| --------- | ---- | ----------- |
| `ts` | string | Timestamp of the operation |
| `objectId` | string | The deleted object ID |
| `deleted` | boolean | Whether the object was deleted |
## Triggers
@@ -0,0 +1,98 @@
import { createLogger } from '@sim/logger'
import { getErrorMessage, toError } from '@sim/utils/errors'
import { type NextRequest, NextResponse } from 'next/server'
import { jsmObjectTypeAttributesContract } from '@/lib/api/contracts/selectors/jsm'
import { parseRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import {
validateAssetsWorkspaceId,
validateJiraCloudId,
} from '@/lib/core/security/input-validation'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { parseAtlassianErrorMessage } from '@/tools/jira/utils'
import { getAssetsApiBaseUrl, getJsmHeaders, resolveAssetsContext } from '@/tools/jsm/utils'
export const dynamic = 'force-dynamic'
const logger = createLogger('JsmAssetsAttributesAPI')
export const POST = withRouteHandler(async (request: NextRequest) => {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
try {
const parsed = await parseRequest(jsmObjectTypeAttributesContract, request, {})
if (!parsed.success) return parsed.response
const {
domain,
accessToken,
cloudId: cloudIdParam,
workspaceId: workspaceIdParam,
objectTypeId,
onlyValueEditable,
query: searchQuery,
} = parsed.data.body
const { cloudId, workspaceId } = await resolveAssetsContext(
domain,
accessToken,
cloudIdParam,
workspaceIdParam
)
const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId')
if (!cloudIdValidation.isValid) {
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
}
const workspaceIdValidation = validateAssetsWorkspaceId(workspaceId, 'workspaceId')
if (!workspaceIdValidation.isValid) {
return NextResponse.json({ error: workspaceIdValidation.error }, { status: 400 })
}
const query = new URLSearchParams()
if (onlyValueEditable !== undefined) {
query.append('onlyValueEditable', String(onlyValueEditable))
}
if (searchQuery) query.append('query', searchQuery)
const url = `${getAssetsApiBaseUrl(cloudId, workspaceId)}/objecttype/${encodeURIComponent(
objectTypeId
)}/attributes${query.toString() ? `?${query.toString()}` : ''}`
const response = await fetch(url, { method: 'GET', headers: getJsmHeaders(accessToken) })
if (!response.ok) {
const errorText = await response.text()
logger.error('Assets API error getting attributes', { status: response.status, errorText })
return NextResponse.json(
{
error: parseAtlassianErrorMessage(response.status, response.statusText, errorText),
details: errorText,
},
{ status: response.status }
)
}
const data = await response.json()
const attributes = Array.isArray(data) ? data : (data.values ?? [])
return NextResponse.json({
success: true,
output: {
ts: new Date().toISOString(),
attributes,
total: attributes.length,
},
})
} catch (error) {
logger.error('Error getting Assets attributes', { error: toError(error).message })
return NextResponse.json(
{ error: getErrorMessage(error, 'Internal server error'), success: false },
{ status: 500 }
)
}
})
@@ -0,0 +1,97 @@
import { createLogger } from '@sim/logger'
import { getErrorMessage, toError } from '@sim/utils/errors'
import { type NextRequest, NextResponse } from 'next/server'
import { jsmListObjectTypesContract } from '@/lib/api/contracts/selectors/jsm'
import { parseRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import {
validateAssetsWorkspaceId,
validateJiraCloudId,
} from '@/lib/core/security/input-validation'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { parseAtlassianErrorMessage } from '@/tools/jira/utils'
import { getAssetsApiBaseUrl, getJsmHeaders, resolveAssetsContext } from '@/tools/jsm/utils'
export const dynamic = 'force-dynamic'
const logger = createLogger('JsmAssetsObjectTypesAPI')
export const POST = withRouteHandler(async (request: NextRequest) => {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
try {
const parsed = await parseRequest(jsmListObjectTypesContract, request, {})
if (!parsed.success) return parsed.response
const {
domain,
accessToken,
cloudId: cloudIdParam,
workspaceId: workspaceIdParam,
schemaId,
excludeAbstract,
} = parsed.data.body
const { cloudId, workspaceId } = await resolveAssetsContext(
domain,
accessToken,
cloudIdParam,
workspaceIdParam
)
const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId')
if (!cloudIdValidation.isValid) {
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
}
const workspaceIdValidation = validateAssetsWorkspaceId(workspaceId, 'workspaceId')
if (!workspaceIdValidation.isValid) {
return NextResponse.json({ error: workspaceIdValidation.error }, { status: 400 })
}
const query = new URLSearchParams()
if (excludeAbstract !== undefined) query.append('excludeAbstract', String(excludeAbstract))
const url = `${getAssetsApiBaseUrl(cloudId, workspaceId)}/objectschema/${encodeURIComponent(
schemaId
)}/objecttypes${query.toString() ? `?${query.toString()}` : ''}`
const response = await fetch(url, { method: 'GET', headers: getJsmHeaders(accessToken) })
if (!response.ok) {
const errorText = await response.text()
logger.error('Assets API error listing object types', {
status: response.status,
errorText,
})
return NextResponse.json(
{
error: parseAtlassianErrorMessage(response.status, response.statusText, errorText),
details: errorText,
},
{ status: response.status }
)
}
const data = await response.json()
const objectTypes = Array.isArray(data) ? data : (data.values ?? [])
return NextResponse.json({
success: true,
output: {
ts: new Date().toISOString(),
objectTypes,
total: objectTypes.length,
},
})
} catch (error) {
logger.error('Error listing Assets object types', { error: toError(error).message })
return NextResponse.json(
{ error: getErrorMessage(error, 'Internal server error'), success: false },
{ status: 500 }
)
}
})
@@ -0,0 +1,93 @@
import { createLogger } from '@sim/logger'
import { getErrorMessage, toError } from '@sim/utils/errors'
import { type NextRequest, NextResponse } from 'next/server'
import { jsmCreateObjectContract } from '@/lib/api/contracts/selectors/jsm'
import { parseRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import {
validateAssetsWorkspaceId,
validateJiraCloudId,
} from '@/lib/core/security/input-validation'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { parseAtlassianErrorMessage } from '@/tools/jira/utils'
import {
getAssetsApiBaseUrl,
getJsmHeaders,
mapAssetObject,
resolveAssetsContext,
} from '@/tools/jsm/utils'
export const dynamic = 'force-dynamic'
const logger = createLogger('JsmAssetsCreateObjectAPI')
export const POST = withRouteHandler(async (request: NextRequest) => {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
try {
const parsed = await parseRequest(jsmCreateObjectContract, request, {})
if (!parsed.success) return parsed.response
const {
domain,
accessToken,
cloudId: cloudIdParam,
workspaceId: workspaceIdParam,
objectTypeId,
attributes,
} = parsed.data.body
const { cloudId, workspaceId } = await resolveAssetsContext(
domain,
accessToken,
cloudIdParam,
workspaceIdParam
)
const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId')
if (!cloudIdValidation.isValid) {
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
}
const workspaceIdValidation = validateAssetsWorkspaceId(workspaceId, 'workspaceId')
if (!workspaceIdValidation.isValid) {
return NextResponse.json({ error: workspaceIdValidation.error }, { status: 400 })
}
const url = `${getAssetsApiBaseUrl(cloudId, workspaceId)}/object/create`
const response = await fetch(url, {
method: 'POST',
headers: getJsmHeaders(accessToken),
body: JSON.stringify({ objectTypeId, attributes }),
})
if (!response.ok) {
const errorText = await response.text()
logger.error('Assets API error creating object', { status: response.status, errorText })
return NextResponse.json(
{
error: parseAtlassianErrorMessage(response.status, response.statusText, errorText),
details: errorText,
},
{ status: response.status }
)
}
const data = await response.json()
return NextResponse.json({
success: true,
output: { ts: new Date().toISOString(), object: mapAssetObject(data) },
})
} catch (error) {
logger.error('Error creating Assets object', { error: toError(error).message })
return NextResponse.json(
{ error: getErrorMessage(error, 'Internal server error'), success: false },
{ status: 500 }
)
}
})
@@ -0,0 +1,81 @@
import { createLogger } from '@sim/logger'
import { getErrorMessage, toError } from '@sim/utils/errors'
import { type NextRequest, NextResponse } from 'next/server'
import { jsmDeleteObjectContract } from '@/lib/api/contracts/selectors/jsm'
import { parseRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import {
validateAssetsWorkspaceId,
validateJiraCloudId,
} from '@/lib/core/security/input-validation'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { parseAtlassianErrorMessage } from '@/tools/jira/utils'
import { getAssetsApiBaseUrl, getJsmHeaders, resolveAssetsContext } from '@/tools/jsm/utils'
export const dynamic = 'force-dynamic'
const logger = createLogger('JsmAssetsDeleteObjectAPI')
export const POST = withRouteHandler(async (request: NextRequest) => {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
try {
const parsed = await parseRequest(jsmDeleteObjectContract, request, {})
if (!parsed.success) return parsed.response
const {
domain,
accessToken,
cloudId: cloudIdParam,
workspaceId: workspaceIdParam,
objectId,
} = parsed.data.body
const { cloudId, workspaceId } = await resolveAssetsContext(
domain,
accessToken,
cloudIdParam,
workspaceIdParam
)
const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId')
if (!cloudIdValidation.isValid) {
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
}
const workspaceIdValidation = validateAssetsWorkspaceId(workspaceId, 'workspaceId')
if (!workspaceIdValidation.isValid) {
return NextResponse.json({ error: workspaceIdValidation.error }, { status: 400 })
}
const url = `${getAssetsApiBaseUrl(cloudId, workspaceId)}/object/${encodeURIComponent(objectId)}`
const response = await fetch(url, { method: 'DELETE', headers: getJsmHeaders(accessToken) })
if (!response.ok) {
const errorText = await response.text()
logger.error('Assets API error deleting object', { status: response.status, errorText })
return NextResponse.json(
{
error: parseAtlassianErrorMessage(response.status, response.statusText, errorText),
details: errorText,
},
{ status: response.status }
)
}
return NextResponse.json({
success: true,
output: { ts: new Date().toISOString(), objectId, deleted: true },
})
} catch (error) {
logger.error('Error deleting Assets object', { error: toError(error).message })
return NextResponse.json(
{ error: getErrorMessage(error, 'Internal server error'), success: false },
{ status: 500 }
)
}
})
@@ -0,0 +1,88 @@
import { createLogger } from '@sim/logger'
import { getErrorMessage, toError } from '@sim/utils/errors'
import { type NextRequest, NextResponse } from 'next/server'
import { jsmGetObjectContract } from '@/lib/api/contracts/selectors/jsm'
import { parseRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import {
validateAssetsWorkspaceId,
validateJiraCloudId,
} from '@/lib/core/security/input-validation'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { parseAtlassianErrorMessage } from '@/tools/jira/utils'
import {
getAssetsApiBaseUrl,
getJsmHeaders,
mapAssetObject,
resolveAssetsContext,
} from '@/tools/jsm/utils'
export const dynamic = 'force-dynamic'
const logger = createLogger('JsmAssetsGetObjectAPI')
export const POST = withRouteHandler(async (request: NextRequest) => {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
try {
const parsed = await parseRequest(jsmGetObjectContract, request, {})
if (!parsed.success) return parsed.response
const {
domain,
accessToken,
cloudId: cloudIdParam,
workspaceId: workspaceIdParam,
objectId,
} = parsed.data.body
const { cloudId, workspaceId } = await resolveAssetsContext(
domain,
accessToken,
cloudIdParam,
workspaceIdParam
)
const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId')
if (!cloudIdValidation.isValid) {
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
}
const workspaceIdValidation = validateAssetsWorkspaceId(workspaceId, 'workspaceId')
if (!workspaceIdValidation.isValid) {
return NextResponse.json({ error: workspaceIdValidation.error }, { status: 400 })
}
const url = `${getAssetsApiBaseUrl(cloudId, workspaceId)}/object/${encodeURIComponent(objectId)}`
const response = await fetch(url, { method: 'GET', headers: getJsmHeaders(accessToken) })
if (!response.ok) {
const errorText = await response.text()
logger.error('Assets API error getting object', { status: response.status, errorText })
return NextResponse.json(
{
error: parseAtlassianErrorMessage(response.status, response.statusText, errorText),
details: errorText,
},
{ status: response.status }
)
}
const data = await response.json()
return NextResponse.json({
success: true,
output: { ts: new Date().toISOString(), object: mapAssetObject(data) },
})
} catch (error) {
logger.error('Error getting Assets object', { error: toError(error).message })
return NextResponse.json(
{ error: getErrorMessage(error, 'Internal server error'), success: false },
{ status: 500 }
)
}
})
@@ -0,0 +1,97 @@
import { createLogger } from '@sim/logger'
import { getErrorMessage, toError } from '@sim/utils/errors'
import { type NextRequest, NextResponse } from 'next/server'
import { jsmUpdateObjectContract } from '@/lib/api/contracts/selectors/jsm'
import { parseRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import {
validateAssetsWorkspaceId,
validateJiraCloudId,
} from '@/lib/core/security/input-validation'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { parseAtlassianErrorMessage } from '@/tools/jira/utils'
import {
getAssetsApiBaseUrl,
getJsmHeaders,
mapAssetObject,
resolveAssetsContext,
} from '@/tools/jsm/utils'
export const dynamic = 'force-dynamic'
const logger = createLogger('JsmAssetsUpdateObjectAPI')
export const POST = withRouteHandler(async (request: NextRequest) => {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
try {
const parsed = await parseRequest(jsmUpdateObjectContract, request, {})
if (!parsed.success) return parsed.response
const {
domain,
accessToken,
cloudId: cloudIdParam,
workspaceId: workspaceIdParam,
objectId,
objectTypeId,
attributes,
} = parsed.data.body
const { cloudId, workspaceId } = await resolveAssetsContext(
domain,
accessToken,
cloudIdParam,
workspaceIdParam
)
const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId')
if (!cloudIdValidation.isValid) {
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
}
const workspaceIdValidation = validateAssetsWorkspaceId(workspaceId, 'workspaceId')
if (!workspaceIdValidation.isValid) {
return NextResponse.json({ error: workspaceIdValidation.error }, { status: 400 })
}
const url = `${getAssetsApiBaseUrl(cloudId, workspaceId)}/object/${encodeURIComponent(objectId)}`
const body: Record<string, unknown> = { attributes }
if (objectTypeId) body.objectTypeId = objectTypeId
const response = await fetch(url, {
method: 'PUT',
headers: getJsmHeaders(accessToken),
body: JSON.stringify(body),
})
if (!response.ok) {
const errorText = await response.text()
logger.error('Assets API error updating object', { status: response.status, errorText })
return NextResponse.json(
{
error: parseAtlassianErrorMessage(response.status, response.statusText, errorText),
details: errorText,
},
{ status: response.status }
)
}
const data = await response.json()
return NextResponse.json({
success: true,
output: { ts: new Date().toISOString(), object: mapAssetObject(data) },
})
} catch (error) {
logger.error('Error updating Assets object', { error: toError(error).message })
return NextResponse.json(
{ error: getErrorMessage(error, 'Internal server error'), success: false },
{ status: 500 }
)
}
})
@@ -0,0 +1,83 @@
import { createLogger } from '@sim/logger'
import { getErrorMessage, toError } from '@sim/utils/errors'
import { type NextRequest, NextResponse } from 'next/server'
import { jsmGetObjectSchemaContract } from '@/lib/api/contracts/selectors/jsm'
import { parseRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import {
validateAssetsWorkspaceId,
validateJiraCloudId,
} from '@/lib/core/security/input-validation'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { parseAtlassianErrorMessage } from '@/tools/jira/utils'
import { getAssetsApiBaseUrl, getJsmHeaders, resolveAssetsContext } from '@/tools/jsm/utils'
export const dynamic = 'force-dynamic'
const logger = createLogger('JsmAssetsSchemaAPI')
export const POST = withRouteHandler(async (request: NextRequest) => {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
try {
const parsed = await parseRequest(jsmGetObjectSchemaContract, request, {})
if (!parsed.success) return parsed.response
const {
domain,
accessToken,
cloudId: cloudIdParam,
workspaceId: workspaceIdParam,
schemaId,
} = parsed.data.body
const { cloudId, workspaceId } = await resolveAssetsContext(
domain,
accessToken,
cloudIdParam,
workspaceIdParam
)
const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId')
if (!cloudIdValidation.isValid) {
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
}
const workspaceIdValidation = validateAssetsWorkspaceId(workspaceId, 'workspaceId')
if (!workspaceIdValidation.isValid) {
return NextResponse.json({ error: workspaceIdValidation.error }, { status: 400 })
}
const url = `${getAssetsApiBaseUrl(cloudId, workspaceId)}/objectschema/${encodeURIComponent(schemaId)}`
const response = await fetch(url, { method: 'GET', headers: getJsmHeaders(accessToken) })
if (!response.ok) {
const errorText = await response.text()
logger.error('Assets API error getting schema', { status: response.status, errorText })
return NextResponse.json(
{
error: parseAtlassianErrorMessage(response.status, response.statusText, errorText),
details: errorText,
},
{ status: response.status }
)
}
const data = await response.json()
return NextResponse.json({
success: true,
output: { ts: new Date().toISOString(), schema: data ?? null },
})
} catch (error) {
logger.error('Error getting Assets schema', { error: toError(error).message })
return NextResponse.json(
{ error: getErrorMessage(error, 'Internal server error'), success: false },
{ status: 500 }
)
}
})
@@ -0,0 +1,97 @@
import { createLogger } from '@sim/logger'
import { getErrorMessage, toError } from '@sim/utils/errors'
import { type NextRequest, NextResponse } from 'next/server'
import { jsmListObjectSchemasContract } from '@/lib/api/contracts/selectors/jsm'
import { parseRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import {
validateAssetsWorkspaceId,
validateJiraCloudId,
} from '@/lib/core/security/input-validation'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { parseAtlassianErrorMessage } from '@/tools/jira/utils'
import { getAssetsApiBaseUrl, getJsmHeaders, resolveAssetsContext } from '@/tools/jsm/utils'
export const dynamic = 'force-dynamic'
const logger = createLogger('JsmAssetsSchemasAPI')
export const POST = withRouteHandler(async (request: NextRequest) => {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
try {
const parsed = await parseRequest(jsmListObjectSchemasContract, request, {})
if (!parsed.success) return parsed.response
const {
domain,
accessToken,
cloudId: cloudIdParam,
workspaceId: workspaceIdParam,
startAt,
maxResults,
includeCounts,
} = parsed.data.body
const { cloudId, workspaceId } = await resolveAssetsContext(
domain,
accessToken,
cloudIdParam,
workspaceIdParam
)
const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId')
if (!cloudIdValidation.isValid) {
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
}
const workspaceIdValidation = validateAssetsWorkspaceId(workspaceId, 'workspaceId')
if (!workspaceIdValidation.isValid) {
return NextResponse.json({ error: workspaceIdValidation.error }, { status: 400 })
}
const query = new URLSearchParams()
if (startAt !== undefined) query.append('startAt', String(startAt))
if (maxResults !== undefined) query.append('maxResults', String(maxResults))
if (includeCounts !== undefined) query.append('includeCounts', String(includeCounts))
const url = `${getAssetsApiBaseUrl(cloudId, workspaceId)}/objectschema/list${
query.toString() ? `?${query.toString()}` : ''
}`
const response = await fetch(url, { method: 'GET', headers: getJsmHeaders(accessToken) })
if (!response.ok) {
const errorText = await response.text()
logger.error('Assets API error listing schemas', { status: response.status, errorText })
return NextResponse.json(
{
error: parseAtlassianErrorMessage(response.status, response.statusText, errorText),
details: errorText,
},
{ status: response.status }
)
}
const data = await response.json()
return NextResponse.json({
success: true,
output: {
ts: new Date().toISOString(),
schemas: data.values ?? [],
total: data.total ?? (data.values?.length || 0),
isLast: data.isLast ?? data.last ?? true,
},
})
} catch (error) {
logger.error('Error listing Assets schemas', { error: toError(error).message })
return NextResponse.json(
{ error: getErrorMessage(error, 'Internal server error'), success: false },
{ status: 500 }
)
}
})
@@ -0,0 +1,122 @@
import { createLogger } from '@sim/logger'
import { getErrorMessage, toError } from '@sim/utils/errors'
import { type NextRequest, NextResponse } from 'next/server'
import { jsmSearchObjectsAqlContract } from '@/lib/api/contracts/selectors/jsm'
import { parseRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import {
validateAssetsWorkspaceId,
validateJiraCloudId,
} from '@/lib/core/security/input-validation'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { parseAtlassianErrorMessage } from '@/tools/jira/utils'
import {
getAssetsApiBaseUrl,
getJsmHeaders,
mapAssetObject,
resolveAssetsContext,
} from '@/tools/jsm/utils'
export const dynamic = 'force-dynamic'
const logger = createLogger('JsmAssetsSearchAPI')
/** Coerce a string|number|boolean param into a number, falling back when unset */
function toNumber(value: string | number | undefined, fallback: number): number {
if (value === undefined) return fallback
const parsed = typeof value === 'number' ? value : Number(value)
return Number.isFinite(parsed) ? parsed : fallback
}
export const POST = withRouteHandler(async (request: NextRequest) => {
const auth = await checkInternalAuth(request)
if (!auth.success || !auth.userId) {
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
}
try {
const parsed = await parseRequest(jsmSearchObjectsAqlContract, request, {})
if (!parsed.success) return parsed.response
const {
domain,
accessToken,
cloudId: cloudIdParam,
workspaceId: workspaceIdParam,
qlQuery,
page,
resultsPerPage,
includeAttributes,
objectTypeId,
objectSchemaId,
} = parsed.data.body
const { cloudId, workspaceId } = await resolveAssetsContext(
domain,
accessToken,
cloudIdParam,
workspaceIdParam
)
const cloudIdValidation = validateJiraCloudId(cloudId, 'cloudId')
if (!cloudIdValidation.isValid) {
return NextResponse.json({ error: cloudIdValidation.error }, { status: 400 })
}
const workspaceIdValidation = validateAssetsWorkspaceId(workspaceId, 'workspaceId')
if (!workspaceIdValidation.isValid) {
return NextResponse.json({ error: workspaceIdValidation.error }, { status: 400 })
}
const includeAttrs =
includeAttributes === undefined ? true : String(includeAttributes) === 'true'
const body: Record<string, unknown> = {
qlQuery,
page: toNumber(page, 1),
resultsPerPage: toNumber(resultsPerPage, 25),
includeAttributes: includeAttrs,
}
if (objectTypeId) body.objectTypeId = objectTypeId
if (objectSchemaId) body.objectSchemaId = objectSchemaId
const url = `${getAssetsApiBaseUrl(cloudId, workspaceId)}/object/aql`
const response = await fetch(url, {
method: 'POST',
headers: getJsmHeaders(accessToken),
body: JSON.stringify(body),
})
if (!response.ok) {
const errorText = await response.text()
logger.error('Assets API error running AQL search', { status: response.status, errorText })
return NextResponse.json(
{
error: parseAtlassianErrorMessage(response.status, response.statusText, errorText),
details: errorText,
},
{ status: response.status }
)
}
const data = await response.json()
return NextResponse.json({
success: true,
output: {
ts: new Date().toISOString(),
objects: Array.isArray(data.objectEntries) ? data.objectEntries.map(mapAssetObject) : [],
total: data.totalFilterCount ?? (data.objectEntries?.length || 0),
pageNumber: data.pageNumber ?? 1,
pageSize: data.pageSize ?? (data.objectEntries?.length || 0),
},
})
} catch (error) {
logger.error('Error running Assets AQL search', { error: toError(error).message })
return NextResponse.json(
{ error: getErrorMessage(error, 'Internal server error'), success: false },
{ status: 500 }
)
}
})
@@ -5,6 +5,38 @@ import { AuthMode, IntegrationType } from '@/blocks/types'
import type { JsmResponse } from '@/tools/jsm/types'
import { getTrigger } from '@/triggers'
/**
* Coerce an optional numeric block input into an integer, returning undefined for
* empty or non-numeric values so no `NaN` reaches the API query string.
*/
function toOptionalInt(value: string | undefined): number | undefined {
if (!value) return undefined
const parsed = Number.parseInt(value, 10)
return Number.isNaN(parsed) ? undefined : parsed
}
/**
* Parse the Assets attributes input into the API payload array. Accepts either a
* JSON string (from the block input) or an already-parsed array (from a dynamic
* reference). Throws a clear error when the value is not a valid array.
*/
function parseAssetAttributes(value: unknown): unknown[] {
if (Array.isArray(value)) return value
if (typeof value === 'string') {
let parsed: unknown
try {
parsed = JSON.parse(value)
} catch {
throw new Error('Attributes must be a valid JSON array')
}
if (!Array.isArray(parsed)) {
throw new Error('Attributes must be a JSON array')
}
return parsed
}
throw new Error('Attributes are required')
}
export const JiraServiceManagementBlock: BlockConfig<JsmResponse> = {
type: 'jira_service_management',
name: 'Jira Service Management',
@@ -57,6 +89,15 @@ export const JiraServiceManagementBlock: BlockConfig<JsmResponse> = {
{ label: 'Externalise Form', id: 'externalise_form' },
{ label: 'Internalise Form', id: 'internalise_form' },
{ label: 'Copy Forms', id: 'copy_forms' },
{ label: 'List Asset Schemas', id: 'list_object_schemas' },
{ label: 'Get Asset Schema', id: 'get_object_schema' },
{ label: 'List Asset Object Types', id: 'list_object_types' },
{ label: 'Get Asset Object Type Attributes', id: 'get_object_type_attributes' },
{ label: 'Search Assets (AQL)', id: 'search_objects_aql' },
{ label: 'Get Asset Object', id: 'get_object' },
{ label: 'Create Asset Object', id: 'create_object' },
{ label: 'Update Asset Object', id: 'update_object' },
{ label: 'Delete Asset Object', id: 'delete_object' },
],
value: () => 'get_service_desks',
},
@@ -564,6 +605,185 @@ Return ONLY the comment text - no explanations.`,
],
},
},
{
id: 'assetSchemaId',
title: 'Schema ID',
type: 'short-input',
placeholder: 'e.g., 1',
required: { field: 'operation', value: ['get_object_schema', 'list_object_types'] },
condition: { field: 'operation', value: ['get_object_schema', 'list_object_types'] },
},
{
id: 'assetObjectTypeId',
title: 'Object Type ID',
type: 'short-input',
placeholder: 'e.g., 23',
required: { field: 'operation', value: ['get_object_type_attributes', 'create_object'] },
condition: {
field: 'operation',
value: [
'get_object_type_attributes',
'create_object',
'update_object',
'search_objects_aql',
],
},
},
{
id: 'assetObjectId',
title: 'Object ID',
type: 'short-input',
placeholder: 'e.g., 1234',
required: { field: 'operation', value: ['get_object', 'update_object', 'delete_object'] },
condition: { field: 'operation', value: ['get_object', 'update_object', 'delete_object'] },
},
{
id: 'assetQlQuery',
title: 'AQL Query',
type: 'long-input',
placeholder: 'objectType = "Host" AND "Operating System" = "Ubuntu"',
required: { field: 'operation', value: 'search_objects_aql' },
condition: { field: 'operation', value: 'search_objects_aql' },
wandConfig: {
enabled: true,
placeholder: 'Describe which assets to find',
prompt:
'Generate an Atlassian Assets AQL (Assets Query Language) query for the user request. Use attribute = "value" comparisons, AND/OR, IN, LIKE, and objectType filters. Example: objectType = "Host" AND Status = "Running". Return ONLY the AQL query - no explanations, no extra text.',
},
},
{
id: 'assetAttributes',
title: 'Attributes',
type: 'long-input',
placeholder:
'[{ "objectTypeAttributeId": "135", "objectAttributeValues": [{ "value": "Server-1" }] }]',
required: { field: 'operation', value: ['create_object', 'update_object'] },
condition: { field: 'operation', value: ['create_object', 'update_object'] },
wandConfig: {
enabled: true,
generationType: 'json-object',
placeholder: 'Describe the attribute values to set',
prompt:
'Generate a JSON array of Atlassian Assets object attributes. Each element is { "objectTypeAttributeId": "<id>", "objectAttributeValues": [{ "value": "<value>" }] }. Use objectTypeAttributeId values from the object type attribute definitions. Return ONLY the JSON array - no explanations, no extra text.',
},
},
{
id: 'assetStartAt',
title: 'Start At',
type: 'short-input',
placeholder: 'Pagination start index (default: 0)',
mode: 'advanced',
condition: { field: 'operation', value: 'list_object_schemas' },
},
{
id: 'assetMaxResults',
title: 'Max Results',
type: 'short-input',
placeholder: 'Maximum schemas to return (default: 25)',
mode: 'advanced',
condition: { field: 'operation', value: 'list_object_schemas' },
},
{
id: 'assetIncludeCounts',
title: 'Include Counts',
type: 'dropdown',
options: [
{ label: 'No', id: 'false' },
{ label: 'Yes', id: 'true' },
],
value: () => 'false',
mode: 'advanced',
condition: { field: 'operation', value: 'list_object_schemas' },
},
{
id: 'assetExcludeAbstract',
title: 'Exclude Abstract Types',
type: 'dropdown',
options: [
{ label: 'No', id: 'false' },
{ label: 'Yes', id: 'true' },
],
value: () => 'false',
mode: 'advanced',
condition: { field: 'operation', value: 'list_object_types' },
},
{
id: 'assetOnlyValueEditable',
title: 'Only Editable Attributes',
type: 'dropdown',
options: [
{ label: 'No', id: 'false' },
{ label: 'Yes', id: 'true' },
],
value: () => 'false',
mode: 'advanced',
condition: { field: 'operation', value: 'get_object_type_attributes' },
},
{
id: 'assetAttributeQuery',
title: 'Attribute Filter',
type: 'short-input',
placeholder: 'Filter attributes by name',
mode: 'advanced',
condition: { field: 'operation', value: 'get_object_type_attributes' },
},
{
id: 'assetPage',
title: 'Page',
type: 'short-input',
placeholder: 'Page number (default: 1)',
mode: 'advanced',
condition: { field: 'operation', value: 'search_objects_aql' },
},
{
id: 'assetResultsPerPage',
title: 'Results Per Page',
type: 'short-input',
placeholder: 'Results per page (default: 25)',
mode: 'advanced',
condition: { field: 'operation', value: 'search_objects_aql' },
},
{
id: 'assetIncludeAttributes',
title: 'Include Attributes',
type: 'dropdown',
options: [
{ label: 'Yes', id: 'true' },
{ label: 'No', id: 'false' },
],
value: () => 'true',
mode: 'advanced',
condition: { field: 'operation', value: 'search_objects_aql' },
},
{
id: 'assetObjectSchemaId',
title: 'Object Schema ID',
type: 'short-input',
placeholder: 'Scope the search to a schema ID',
mode: 'advanced',
condition: { field: 'operation', value: 'search_objects_aql' },
},
{
id: 'assetWorkspaceId',
title: 'Assets Workspace ID',
type: 'short-input',
placeholder: 'Override the auto-resolved Assets workspace',
mode: 'advanced',
condition: {
field: 'operation',
value: [
'list_object_schemas',
'get_object_schema',
'list_object_types',
'get_object_type_attributes',
'search_objects_aql',
'get_object',
'create_object',
'update_object',
'delete_object',
],
},
},
...getTrigger('jsm_request_created').subBlocks,
...getTrigger('jsm_request_updated').subBlocks,
...getTrigger('jsm_request_commented').subBlocks,
@@ -606,6 +826,15 @@ Return ONLY the comment text - no explanations.`,
'jsm_externalise_form',
'jsm_internalise_form',
'jsm_copy_forms',
'jsm_list_object_schemas',
'jsm_get_object_schema',
'jsm_list_object_types',
'jsm_get_object_type_attributes',
'jsm_search_objects_aql',
'jsm_get_object',
'jsm_create_object',
'jsm_update_object',
'jsm_delete_object',
],
config: {
tool: (params) => {
@@ -678,6 +907,24 @@ Return ONLY the comment text - no explanations.`,
return 'jsm_internalise_form'
case 'copy_forms':
return 'jsm_copy_forms'
case 'list_object_schemas':
return 'jsm_list_object_schemas'
case 'get_object_schema':
return 'jsm_get_object_schema'
case 'list_object_types':
return 'jsm_list_object_types'
case 'get_object_type_attributes':
return 'jsm_get_object_type_attributes'
case 'search_objects_aql':
return 'jsm_search_objects_aql'
case 'get_object':
return 'jsm_get_object'
case 'create_object':
return 'jsm_create_object'
case 'update_object':
return 'jsm_update_object'
case 'delete_object':
return 'jsm_delete_object'
default:
return 'jsm_get_service_desks'
}
@@ -688,6 +935,13 @@ Return ONLY the comment text - no explanations.`,
domain: params.domain,
}
// Assets tools accept an optional workspaceId override; when omitted the
// route resolves the site's single Assets workspace automatically.
const assetBaseParams = {
...baseParams,
workspaceId: params.assetWorkspaceId || undefined,
}
switch (params.operation) {
case 'get_service_desks':
return {
@@ -1109,6 +1363,79 @@ Return ONLY the comment text - no explanations.`,
})()
: undefined,
}
case 'list_object_schemas':
return {
...assetBaseParams,
startAt: toOptionalInt(params.assetStartAt),
maxResults: toOptionalInt(params.assetMaxResults),
includeCounts: params.assetIncludeCounts === 'true' ? true : undefined,
}
case 'get_object_schema':
if (!params.assetSchemaId) {
throw new Error('Schema ID is required')
}
return { ...assetBaseParams, schemaId: params.assetSchemaId }
case 'list_object_types':
if (!params.assetSchemaId) {
throw new Error('Schema ID is required')
}
return {
...assetBaseParams,
schemaId: params.assetSchemaId,
excludeAbstract: params.assetExcludeAbstract === 'true' ? true : undefined,
}
case 'get_object_type_attributes':
if (!params.assetObjectTypeId) {
throw new Error('Object type ID is required')
}
return {
...assetBaseParams,
objectTypeId: params.assetObjectTypeId,
onlyValueEditable: params.assetOnlyValueEditable === 'true' ? true : undefined,
query: params.assetAttributeQuery || undefined,
}
case 'search_objects_aql':
if (!params.assetQlQuery) {
throw new Error('AQL query is required')
}
return {
...assetBaseParams,
qlQuery: params.assetQlQuery,
page: toOptionalInt(params.assetPage),
resultsPerPage: toOptionalInt(params.assetResultsPerPage),
includeAttributes: params.assetIncludeAttributes === 'false' ? false : undefined,
objectTypeId: params.assetObjectTypeId || undefined,
objectSchemaId: params.assetObjectSchemaId || undefined,
}
case 'get_object':
if (!params.assetObjectId) {
throw new Error('Object ID is required')
}
return { ...assetBaseParams, objectId: params.assetObjectId }
case 'create_object':
if (!params.assetObjectTypeId) {
throw new Error('Object type ID is required')
}
return {
...assetBaseParams,
objectTypeId: params.assetObjectTypeId,
attributes: parseAssetAttributes(params.assetAttributes),
}
case 'update_object':
if (!params.assetObjectId) {
throw new Error('Object ID is required')
}
return {
...assetBaseParams,
objectId: params.assetObjectId,
attributes: parseAssetAttributes(params.assetAttributes),
objectTypeId: params.assetObjectTypeId || undefined,
}
case 'delete_object':
if (!params.assetObjectId) {
throw new Error('Object ID is required')
}
return { ...assetBaseParams, objectId: params.assetObjectId }
default:
return baseParams
}
@@ -1167,6 +1494,25 @@ Return ONLY the comment text - no explanations.`,
searchQuery: { type: 'string', description: 'Filter request types by name' },
groupId: { type: 'string', description: 'Filter by request type group ID' },
expand: { type: 'string', description: 'Comma-separated fields to expand' },
assetSchemaId: { type: 'string', description: 'Assets object schema ID' },
assetObjectTypeId: { type: 'string', description: 'Assets object type ID' },
assetObjectId: { type: 'string', description: 'Assets object ID' },
assetQlQuery: { type: 'string', description: 'AQL query string' },
assetAttributes: { type: 'string', description: 'JSON array of Assets object attributes' },
assetStartAt: { type: 'string', description: 'Schema pagination start index' },
assetMaxResults: { type: 'string', description: 'Maximum schemas to return' },
assetIncludeCounts: { type: 'string', description: 'Include object/type counts per schema' },
assetExcludeAbstract: { type: 'string', description: 'Exclude abstract object types' },
assetOnlyValueEditable: { type: 'string', description: 'Return only editable attributes' },
assetAttributeQuery: { type: 'string', description: 'Filter attributes by name' },
assetPage: { type: 'string', description: 'AQL search page number' },
assetResultsPerPage: { type: 'string', description: 'AQL search results per page' },
assetIncludeAttributes: { type: 'string', description: 'Include attribute values in results' },
assetObjectSchemaId: { type: 'string', description: 'Scope AQL search to a schema ID' },
assetWorkspaceId: {
type: 'string',
description: 'Override the auto-resolved Assets workspace ID',
},
},
outputs: {
ts: { type: 'string', description: 'Timestamp of the operation' },
@@ -1250,6 +1596,30 @@ Return ONLY the comment text - no explanations.`,
errors: { type: 'json', description: 'Array of errors from copy forms operation' },
sourceIssueIdOrKey: { type: 'string', description: 'Source issue ID or key' },
targetIssueIdOrKey: { type: 'string', description: 'Target issue ID or key' },
schemas: {
type: 'json',
description: 'Array of Assets object schemas (id, name, objectSchemaKey, status)',
},
schema: { type: 'json', description: 'Single Assets object schema' },
objectTypes: {
type: 'json',
description: 'Array of Assets object types (id, name, objectSchemaId, objectCount)',
},
attributes: {
type: 'json',
description:
'Array of object type attribute definitions (id, name, type, minimumCardinality)',
},
objects: {
type: 'json',
description: 'Array of Assets objects from an AQL search (id, label, objectKey, attributes)',
},
object: {
type: 'json',
description: 'Single Assets object (id, label, objectKey, objectType, attributes)',
},
objectId: { type: 'string', description: 'Assets object ID (delete operation)' },
isLast: { type: 'boolean', description: 'Whether this is the last page of schemas' },
},
triggers: {
enabled: true,
+111
View File
@@ -185,6 +185,80 @@ export const jsmCopyFormsBodySchema = jsmBaseBodySchema.extend({
formIds: z.array(z.string(), { error: 'formIds must be an array of form UUIDs' }).optional(),
})
const jsmAssetsBaseBodySchema = jsmBaseBodySchema.extend({
workspaceId: z.string().optional(),
})
const jsmAssetsPaginationField = z.union([z.string(), z.number()]).optional()
export const jsmListObjectSchemasBodySchema = jsmAssetsBaseBodySchema.extend({
startAt: jsmAssetsPaginationField,
maxResults: jsmAssetsPaginationField,
includeCounts: z.union([z.string(), z.boolean()]).optional(),
})
export const jsmObjectSchemaBodySchema = jsmAssetsBaseBodySchema.extend({
schemaId: z.string({ error: 'Schema ID is required' }).min(1, 'Schema ID is required'),
})
export const jsmObjectTypesBodySchema = jsmAssetsBaseBodySchema.extend({
schemaId: z.string({ error: 'Schema ID is required' }).min(1, 'Schema ID is required'),
excludeAbstract: z.union([z.string(), z.boolean()]).optional(),
})
export const jsmObjectTypeAttributesBodySchema = jsmAssetsBaseBodySchema.extend({
objectTypeId: z
.string({ error: 'Object type ID is required' })
.min(1, 'Object type ID is required'),
onlyValueEditable: z.union([z.string(), z.boolean()]).optional(),
query: z.string().optional(),
})
export const jsmSearchObjectsAqlBodySchema = jsmAssetsBaseBodySchema.extend({
qlQuery: z.string({ error: 'AQL query is required' }).min(1, 'AQL query is required'),
page: jsmAssetsPaginationField,
resultsPerPage: jsmAssetsPaginationField,
includeAttributes: z.union([z.string(), z.boolean()]).optional(),
objectTypeId: z.string().optional(),
objectSchemaId: z.string().optional(),
})
export const jsmGetObjectBodySchema = jsmAssetsBaseBodySchema.extend({
objectId: z.string({ error: 'Object ID is required' }).min(1, 'Object ID is required'),
})
const jsmAssetAttributeInputSchema = z.object({
objectTypeAttributeId: z
.string({ error: 'objectTypeAttributeId is required' })
.min(1, 'objectTypeAttributeId is required'),
objectAttributeValues: z
.array(z.object({ value: z.unknown() }), {
error: 'objectAttributeValues must be an array of { value } entries',
})
.min(1, 'Each attribute needs at least one value'),
})
export const jsmCreateObjectBodySchema = jsmAssetsBaseBodySchema.extend({
objectTypeId: z
.string({ error: 'Object type ID is required' })
.min(1, 'Object type ID is required'),
attributes: z
.array(jsmAssetAttributeInputSchema, { error: 'attributes is required' })
.min(1, 'At least one attribute is required'),
})
export const jsmUpdateObjectBodySchema = jsmAssetsBaseBodySchema.extend({
objectId: z.string({ error: 'Object ID is required' }).min(1, 'Object ID is required'),
objectTypeId: z.string().optional(),
attributes: z
.array(jsmAssetAttributeInputSchema, { error: 'attributes is required' })
.min(1, 'At least one attribute is required'),
})
export const jsmDeleteObjectBodySchema = jsmAssetsBaseBodySchema.extend({
objectId: z.string({ error: 'Object ID is required' }).min(1, 'Object ID is required'),
})
export const defineJsmToolContract = <TBody extends z.ZodType>(path: string, body: TBody) =>
definePostSelector(path, body, z.unknown())
@@ -314,6 +388,43 @@ export const jsmCopyFormsContract = defineJsmToolContract(
jsmCopyFormsBodySchema
)
export const jsmListObjectSchemasContract = defineJsmToolContract(
'/api/tools/jsm/assets/schemas',
jsmListObjectSchemasBodySchema
)
export const jsmGetObjectSchemaContract = defineJsmToolContract(
'/api/tools/jsm/assets/schema',
jsmObjectSchemaBodySchema
)
export const jsmListObjectTypesContract = defineJsmToolContract(
'/api/tools/jsm/assets/object-types',
jsmObjectTypesBodySchema
)
export const jsmObjectTypeAttributesContract = defineJsmToolContract(
'/api/tools/jsm/assets/attributes',
jsmObjectTypeAttributesBodySchema
)
export const jsmSearchObjectsAqlContract = defineJsmToolContract(
'/api/tools/jsm/assets/search',
jsmSearchObjectsAqlBodySchema
)
export const jsmGetObjectContract = defineJsmToolContract(
'/api/tools/jsm/assets/object/get',
jsmGetObjectBodySchema
)
export const jsmCreateObjectContract = defineJsmToolContract(
'/api/tools/jsm/assets/object/create',
jsmCreateObjectBodySchema
)
export const jsmUpdateObjectContract = defineJsmToolContract(
'/api/tools/jsm/assets/object/update',
jsmUpdateObjectBodySchema
)
export const jsmDeleteObjectContract = defineJsmToolContract(
'/api/tools/jsm/assets/object/delete',
jsmDeleteObjectBodySchema
)
export type JsmServiceDesksBody = ContractBody<typeof jsmServiceDesksContract>
export type JsmQueuesBody = ContractBody<typeof jsmQueuesContract>
export type JsmRequestTypesBody = ContractBody<typeof jsmRequestTypesContract>
@@ -627,6 +627,27 @@ export function validateJiraCloudId(
})
}
/**
* Validates an Atlassian Assets workspace ID (a UUID-shaped, hyphenated
* alphanumeric identifier) before it is interpolated into an API path.
*
* @param value - The Assets workspace ID to validate
* @param paramName - Name of the parameter for error messages
* @returns ValidationResult
*/
export function validateAssetsWorkspaceId(
value: string | null | undefined,
paramName = 'workspaceId'
): ValidationResult {
return validatePathSegment(value, {
paramName,
allowHyphens: true,
allowUnderscores: false,
allowDots: false,
maxLength: 100,
})
}
/**
* Validates Jira issue keys (format: PROJECT-123 or PROJECT-KEY-123)
*
+37 -1
View File
@@ -8445,9 +8445,45 @@
{
"name": "Copy Forms",
"description": "Copy forms from one Jira issue to another"
},
{
"name": "List Asset Schemas",
"description": "List Assets (Insight/CMDB) object schemas in Jira Service Management"
},
{
"name": "Get Asset Schema",
"description": "Get a single Assets (Insight/CMDB) object schema by ID"
},
{
"name": "List Asset Object Types",
"description": "List object types within an Assets (Insight/CMDB) object schema"
},
{
"name": "Get Asset Object Type Attributes",
"description": "Get the attribute definitions for an Assets (Insight/CMDB) object type. Use the returned attribute IDs to build create/update payloads or map columns."
},
{
"name": "Search Assets (AQL)",
"description": "Search Assets (Insight/CMDB) objects using AQL (Assets Query Language), e.g. objectType = "
},
{
"name": "Get Asset Object",
"description": "Get a single Assets (Insight/CMDB) object by ID, including its attribute values"
},
{
"name": "Create Asset Object",
"description": "Create an Assets (Insight/CMDB) object of a given object type. Attributes use objectTypeAttributeId values from the object type definition."
},
{
"name": "Update Asset Object",
"description": "Update an existing Assets (Insight/CMDB) object. Provide the attributes to change using their objectTypeAttributeId values."
},
{
"name": "Delete Asset Object",
"description": "Delete an Assets (Insight/CMDB) object by ID"
}
],
"operationCount": 34,
"operationCount": 43,
"triggers": [
{
"id": "jsm_request_created",
+6
View File
@@ -547,6 +547,12 @@ export const OAUTH_PROVIDERS: Record<string, OAuthProviderConfig> = {
'write:request.participant:jira-service-management',
'read:request.approval:jira-service-management',
'write:request.approval:jira-service-management',
'read:cmdb-object:jira',
'write:cmdb-object:jira',
'delete:cmdb-object:jira',
'read:cmdb-schema:jira',
'read:cmdb-type:jira',
'read:cmdb-attribute:jira',
],
},
},
+6
View File
@@ -204,6 +204,12 @@ export const SCOPE_DESCRIPTIONS: Record<string, string> = {
'Add and remove participants from customer requests',
'read:request.approval:jira-service-management': 'View approvals on customer requests',
'write:request.approval:jira-service-management': 'Approve or decline customer requests',
'read:cmdb-object:jira': 'View Assets objects and run AQL searches',
'write:cmdb-object:jira': 'Create and update Assets objects',
'delete:cmdb-object:jira': 'Delete Assets objects',
'read:cmdb-schema:jira': 'View Assets object schemas',
'read:cmdb-type:jira': 'View Assets object types',
'read:cmdb-attribute:jira': 'View Assets object type attributes',
// Microsoft scopes
'User.Read': 'Read Microsoft user',
+97
View File
@@ -0,0 +1,97 @@
import type { JsmCreateObjectParams, JsmCreateObjectResponse } from '@/tools/jsm/types'
import { ASSET_OBJECT_PROPERTIES } from '@/tools/jsm/types'
import type { ToolConfig } from '@/tools/types'
export const jsmCreateObjectTool: ToolConfig<JsmCreateObjectParams, JsmCreateObjectResponse> = {
id: 'jsm_create_object',
name: 'JSM Create Asset Object',
description:
'Create an Assets (Insight/CMDB) object of a given object type. Attributes use objectTypeAttributeId values from the object type definition.',
version: '1.0.0',
oauth: {
required: true,
provider: 'jira',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'OAuth access token for Jira Service Management',
},
domain: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Your Jira domain (e.g., yourcompany.atlassian.net)',
},
cloudId: {
type: 'string',
required: false,
visibility: 'hidden',
description: 'Jira Cloud ID for the instance',
},
workspaceId: {
type: 'string',
required: false,
visibility: 'hidden',
description: 'Assets workspace ID (resolved automatically when omitted)',
},
objectTypeId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The object type ID to create the object under',
},
attributes: {
type: 'json',
required: true,
visibility: 'user-or-llm',
description:
'Array of attributes: [{ objectTypeAttributeId, objectAttributeValues: [{ value }] }]',
},
},
request: {
url: '/api/tools/jsm/assets/object/create',
method: 'POST',
headers: () => ({ 'Content-Type': 'application/json' }),
body: (params) => ({
domain: params.domain,
accessToken: params.accessToken,
cloudId: params.cloudId,
workspaceId: params.workspaceId,
objectTypeId: params.objectTypeId?.trim(),
attributes: params.attributes,
}),
},
transformResponse: async (response: Response) => {
const responseText = await response.text()
if (!responseText) {
return {
success: false,
output: { ts: new Date().toISOString(), object: null },
error: 'Empty response from API',
}
}
const data = JSON.parse(responseText)
if (data.success && data.output) return data
return {
success: data.success || false,
output: data.output || { ts: new Date().toISOString(), object: null },
error: data.error,
}
},
outputs: {
ts: { type: 'string', description: 'Timestamp of the operation' },
object: {
type: 'json',
description: 'The created Assets object',
properties: ASSET_OBJECT_PROPERTIES,
},
},
}
+84
View File
@@ -0,0 +1,84 @@
import type { JsmDeleteObjectParams, JsmDeleteObjectResponse } from '@/tools/jsm/types'
import type { ToolConfig } from '@/tools/types'
export const jsmDeleteObjectTool: ToolConfig<JsmDeleteObjectParams, JsmDeleteObjectResponse> = {
id: 'jsm_delete_object',
name: 'JSM Delete Asset Object',
description: 'Delete an Assets (Insight/CMDB) object by ID',
version: '1.0.0',
oauth: {
required: true,
provider: 'jira',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'OAuth access token for Jira Service Management',
},
domain: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Your Jira domain (e.g., yourcompany.atlassian.net)',
},
cloudId: {
type: 'string',
required: false,
visibility: 'hidden',
description: 'Jira Cloud ID for the instance',
},
workspaceId: {
type: 'string',
required: false,
visibility: 'hidden',
description: 'Assets workspace ID (resolved automatically when omitted)',
},
objectId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The Assets object ID to delete',
},
},
request: {
url: '/api/tools/jsm/assets/object/delete',
method: 'POST',
headers: () => ({ 'Content-Type': 'application/json' }),
body: (params) => ({
domain: params.domain,
accessToken: params.accessToken,
cloudId: params.cloudId,
workspaceId: params.workspaceId,
objectId: params.objectId?.trim(),
}),
},
transformResponse: async (response: Response) => {
const responseText = await response.text()
if (!responseText) {
return {
success: false,
output: { ts: new Date().toISOString(), objectId: '', deleted: false },
error: 'Empty response from API',
}
}
const data = JSON.parse(responseText)
if (data.success && data.output) return data
return {
success: data.success || false,
output: data.output || { ts: new Date().toISOString(), objectId: '', deleted: false },
error: data.error,
}
},
outputs: {
ts: { type: 'string', description: 'Timestamp of the operation' },
objectId: { type: 'string', description: 'The deleted object ID' },
deleted: { type: 'boolean', description: 'Whether the object was deleted' },
},
}
+88
View File
@@ -0,0 +1,88 @@
import type { JsmGetObjectParams, JsmGetObjectResponse } from '@/tools/jsm/types'
import { ASSET_OBJECT_PROPERTIES } from '@/tools/jsm/types'
import type { ToolConfig } from '@/tools/types'
export const jsmGetObjectTool: ToolConfig<JsmGetObjectParams, JsmGetObjectResponse> = {
id: 'jsm_get_object',
name: 'JSM Get Asset Object',
description: 'Get a single Assets (Insight/CMDB) object by ID, including its attribute values',
version: '1.0.0',
oauth: {
required: true,
provider: 'jira',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'OAuth access token for Jira Service Management',
},
domain: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Your Jira domain (e.g., yourcompany.atlassian.net)',
},
cloudId: {
type: 'string',
required: false,
visibility: 'hidden',
description: 'Jira Cloud ID for the instance',
},
workspaceId: {
type: 'string',
required: false,
visibility: 'hidden',
description: 'Assets workspace ID (resolved automatically when omitted)',
},
objectId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The Assets object ID',
},
},
request: {
url: '/api/tools/jsm/assets/object/get',
method: 'POST',
headers: () => ({ 'Content-Type': 'application/json' }),
body: (params) => ({
domain: params.domain,
accessToken: params.accessToken,
cloudId: params.cloudId,
workspaceId: params.workspaceId,
objectId: params.objectId?.trim(),
}),
},
transformResponse: async (response: Response) => {
const responseText = await response.text()
if (!responseText) {
return {
success: false,
output: { ts: new Date().toISOString(), object: null },
error: 'Empty response from API',
}
}
const data = JSON.parse(responseText)
if (data.success && data.output) return data
return {
success: data.success || false,
output: data.output || { ts: new Date().toISOString(), object: null },
error: data.error,
}
},
outputs: {
ts: { type: 'string', description: 'Timestamp of the operation' },
object: {
type: 'json',
description: 'The Assets object',
properties: ASSET_OBJECT_PROPERTIES,
},
},
}
+102
View File
@@ -0,0 +1,102 @@
import type { JsmGetObjectSchemaParams, JsmGetObjectSchemaResponse } from '@/tools/jsm/types'
import type { ToolConfig } from '@/tools/types'
export const jsmGetObjectSchemaTool: ToolConfig<
JsmGetObjectSchemaParams,
JsmGetObjectSchemaResponse
> = {
id: 'jsm_get_object_schema',
name: 'JSM Get Asset Schema',
description: 'Get a single Assets (Insight/CMDB) object schema by ID',
version: '1.0.0',
oauth: {
required: true,
provider: 'jira',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'OAuth access token for Jira Service Management',
},
domain: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Your Jira domain (e.g., yourcompany.atlassian.net)',
},
cloudId: {
type: 'string',
required: false,
visibility: 'hidden',
description: 'Jira Cloud ID for the instance',
},
workspaceId: {
type: 'string',
required: false,
visibility: 'hidden',
description: 'Assets workspace ID (resolved automatically when omitted)',
},
schemaId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The Assets object schema ID',
},
},
request: {
url: '/api/tools/jsm/assets/schema',
method: 'POST',
headers: () => ({ 'Content-Type': 'application/json' }),
body: (params) => ({
domain: params.domain,
accessToken: params.accessToken,
cloudId: params.cloudId,
workspaceId: params.workspaceId,
schemaId: params.schemaId?.trim(),
}),
},
transformResponse: async (response: Response) => {
const responseText = await response.text()
if (!responseText) {
return {
success: false,
output: { ts: new Date().toISOString(), schema: null },
error: 'Empty response from API',
}
}
const data = JSON.parse(responseText)
if (data.success && data.output) return data
return {
success: data.success || false,
output: data.output || { ts: new Date().toISOString(), schema: null },
error: data.error,
}
},
outputs: {
ts: { type: 'string', description: 'Timestamp of the operation' },
schema: {
type: 'json',
description: 'The Assets object schema',
properties: {
id: { type: 'string', description: 'Schema ID' },
name: { type: 'string', description: 'Schema name' },
objectSchemaKey: { type: 'string', description: 'Schema key' },
status: { type: 'string', description: 'Schema status' },
description: { type: 'string', description: 'Schema description', optional: true },
objectCount: { type: 'number', description: 'Number of objects', optional: true },
objectTypeCount: {
type: 'number',
description: 'Number of object types',
optional: true,
},
},
},
},
}
@@ -0,0 +1,136 @@
import type {
JsmGetObjectTypeAttributesParams,
JsmGetObjectTypeAttributesResponse,
} from '@/tools/jsm/types'
import type { ToolConfig } from '@/tools/types'
export const jsmGetObjectTypeAttributesTool: ToolConfig<
JsmGetObjectTypeAttributesParams,
JsmGetObjectTypeAttributesResponse
> = {
id: 'jsm_get_object_type_attributes',
name: 'JSM Get Asset Object Type Attributes',
description:
'Get the attribute definitions for an Assets (Insight/CMDB) object type. Use the returned attribute IDs to build create/update payloads or map columns.',
version: '1.0.0',
oauth: {
required: true,
provider: 'jira',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'OAuth access token for Jira Service Management',
},
domain: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Your Jira domain (e.g., yourcompany.atlassian.net)',
},
cloudId: {
type: 'string',
required: false,
visibility: 'hidden',
description: 'Jira Cloud ID for the instance',
},
workspaceId: {
type: 'string',
required: false,
visibility: 'hidden',
description: 'Assets workspace ID (resolved automatically when omitted)',
},
objectTypeId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The Assets object type ID',
},
onlyValueEditable: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Return only attributes whose values can be edited',
},
query: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Filter attributes by a search query',
},
},
request: {
url: '/api/tools/jsm/assets/attributes',
method: 'POST',
headers: () => ({ 'Content-Type': 'application/json' }),
body: (params) => ({
domain: params.domain,
accessToken: params.accessToken,
cloudId: params.cloudId,
workspaceId: params.workspaceId,
objectTypeId: params.objectTypeId?.trim(),
onlyValueEditable: params.onlyValueEditable,
query: params.query,
}),
},
transformResponse: async (response: Response) => {
const responseText = await response.text()
if (!responseText) {
return {
success: false,
output: { ts: new Date().toISOString(), attributes: [], total: 0 },
error: 'Empty response from API',
}
}
const data = JSON.parse(responseText)
if (data.success && data.output) return data
return {
success: data.success || false,
output: data.output || { ts: new Date().toISOString(), attributes: [], total: 0 },
error: data.error,
}
},
outputs: {
ts: { type: 'string', description: 'Timestamp of the operation' },
attributes: {
type: 'array',
description: 'Attribute definitions for the object type',
items: {
type: 'object',
properties: {
id: {
type: 'string',
description: 'Attribute definition ID — use as objectTypeAttributeId in create/update',
},
name: { type: 'string', description: 'Attribute name' },
label: { type: 'boolean', description: 'Whether this attribute is the object label' },
type: { type: 'number', description: 'Data type discriminator (integer enum)' },
defaultType: {
type: 'json',
description: 'Default data type { id, name }',
optional: true,
},
editable: { type: 'boolean', description: 'Whether the value is editable' },
minimumCardinality: {
type: 'number',
description: 'Minimum number of values (>= 1 means required)',
},
maximumCardinality: { type: 'number', description: 'Maximum number of values' },
uniqueAttribute: {
type: 'boolean',
description: 'Whether values must be unique',
optional: true,
},
},
},
},
total: { type: 'number', description: 'Total number of attributes' },
},
}
+18
View File
@@ -5,9 +5,11 @@ import { jsmAddParticipantsTool } from '@/tools/jsm/add_participants'
import { jsmAnswerApprovalTool } from '@/tools/jsm/answer_approval'
import { jsmAttachFormTool } from '@/tools/jsm/attach_form'
import { jsmCopyFormsTool } from '@/tools/jsm/copy_forms'
import { jsmCreateObjectTool } from '@/tools/jsm/create_object'
import { jsmCreateOrganizationTool } from '@/tools/jsm/create_organization'
import { jsmCreateRequestTool } from '@/tools/jsm/create_request'
import { jsmDeleteFormTool } from '@/tools/jsm/delete_form'
import { jsmDeleteObjectTool } from '@/tools/jsm/delete_object'
import { jsmExternaliseFormTool } from '@/tools/jsm/externalise_form'
import { jsmGetApprovalsTool } from '@/tools/jsm/get_approvals'
import { jsmGetCommentsTool } from '@/tools/jsm/get_comments'
@@ -17,6 +19,9 @@ import { jsmGetFormAnswersTool } from '@/tools/jsm/get_form_answers'
import { jsmGetFormStructureTool } from '@/tools/jsm/get_form_structure'
import { jsmGetFormTemplatesTool } from '@/tools/jsm/get_form_templates'
import { jsmGetIssueFormsTool } from '@/tools/jsm/get_issue_forms'
import { jsmGetObjectTool } from '@/tools/jsm/get_object'
import { jsmGetObjectSchemaTool } from '@/tools/jsm/get_object_schema'
import { jsmGetObjectTypeAttributesTool } from '@/tools/jsm/get_object_type_attributes'
import { jsmGetOrganizationsTool } from '@/tools/jsm/get_organizations'
import { jsmGetParticipantsTool } from '@/tools/jsm/get_participants'
import { jsmGetQueuesTool } from '@/tools/jsm/get_queues'
@@ -28,10 +33,14 @@ import { jsmGetServiceDesksTool } from '@/tools/jsm/get_service_desks'
import { jsmGetSlaTool } from '@/tools/jsm/get_sla'
import { jsmGetTransitionsTool } from '@/tools/jsm/get_transitions'
import { jsmInternaliseFormTool } from '@/tools/jsm/internalise_form'
import { jsmListObjectSchemasTool } from '@/tools/jsm/list_object_schemas'
import { jsmListObjectTypesTool } from '@/tools/jsm/list_object_types'
import { jsmReopenFormTool } from '@/tools/jsm/reopen_form'
import { jsmSaveFormAnswersTool } from '@/tools/jsm/save_form_answers'
import { jsmSearchObjectsAqlTool } from '@/tools/jsm/search_objects_aql'
import { jsmSubmitFormTool } from '@/tools/jsm/submit_form'
import { jsmTransitionRequestTool } from '@/tools/jsm/transition_request'
import { jsmUpdateObjectTool } from '@/tools/jsm/update_object'
export {
jsmAddCommentTool,
@@ -41,9 +50,11 @@ export {
jsmAnswerApprovalTool,
jsmAttachFormTool,
jsmCopyFormsTool,
jsmCreateObjectTool,
jsmCreateOrganizationTool,
jsmCreateRequestTool,
jsmDeleteFormTool,
jsmDeleteObjectTool,
jsmExternaliseFormTool,
jsmGetApprovalsTool,
jsmGetCommentsTool,
@@ -53,6 +64,9 @@ export {
jsmGetFormStructureTool,
jsmGetFormTemplatesTool,
jsmGetIssueFormsTool,
jsmGetObjectTool,
jsmGetObjectSchemaTool,
jsmGetObjectTypeAttributesTool,
jsmGetOrganizationsTool,
jsmGetParticipantsTool,
jsmGetQueuesTool,
@@ -64,8 +78,12 @@ export {
jsmGetSlaTool,
jsmGetTransitionsTool,
jsmInternaliseFormTool,
jsmListObjectSchemasTool,
jsmListObjectTypesTool,
jsmReopenFormTool,
jsmSaveFormAnswersTool,
jsmSearchObjectsAqlTool,
jsmSubmitFormTool,
jsmTransitionRequestTool,
jsmUpdateObjectTool,
}
+126
View File
@@ -0,0 +1,126 @@
import type { JsmListObjectSchemasParams, JsmListObjectSchemasResponse } from '@/tools/jsm/types'
import type { ToolConfig } from '@/tools/types'
export const jsmListObjectSchemasTool: ToolConfig<
JsmListObjectSchemasParams,
JsmListObjectSchemasResponse
> = {
id: 'jsm_list_object_schemas',
name: 'JSM List Asset Schemas',
description: 'List Assets (Insight/CMDB) object schemas in Jira Service Management',
version: '1.0.0',
oauth: {
required: true,
provider: 'jira',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'OAuth access token for Jira Service Management',
},
domain: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Your Jira domain (e.g., yourcompany.atlassian.net)',
},
cloudId: {
type: 'string',
required: false,
visibility: 'hidden',
description: 'Jira Cloud ID for the instance',
},
workspaceId: {
type: 'string',
required: false,
visibility: 'hidden',
description: 'Assets workspace ID (resolved automatically when omitted)',
},
startAt: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Pagination start index (e.g., 0, 50)',
},
maxResults: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Maximum schemas to return (e.g., 25, 50)',
},
includeCounts: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Include object and object-type counts per schema',
},
},
request: {
url: '/api/tools/jsm/assets/schemas',
method: 'POST',
headers: () => ({ 'Content-Type': 'application/json' }),
body: (params) => ({
domain: params.domain,
accessToken: params.accessToken,
cloudId: params.cloudId,
workspaceId: params.workspaceId,
startAt: params.startAt,
maxResults: params.maxResults,
includeCounts: params.includeCounts,
}),
},
transformResponse: async (response: Response) => {
const responseText = await response.text()
if (!responseText) {
return {
success: false,
output: { ts: new Date().toISOString(), schemas: [], total: 0, isLast: true },
error: 'Empty response from API',
}
}
const data = JSON.parse(responseText)
if (data.success && data.output) return data
return {
success: data.success || false,
output: data.output || {
ts: new Date().toISOString(),
schemas: [],
total: 0,
isLast: true,
},
error: data.error,
}
},
outputs: {
ts: { type: 'string', description: 'Timestamp of the operation' },
schemas: {
type: 'array',
description: 'List of Assets object schemas',
items: {
type: 'object',
properties: {
id: { type: 'string', description: 'Schema ID' },
name: { type: 'string', description: 'Schema name' },
objectSchemaKey: { type: 'string', description: 'Schema key' },
status: { type: 'string', description: 'Schema status' },
description: { type: 'string', description: 'Schema description', optional: true },
objectCount: { type: 'number', description: 'Number of objects', optional: true },
objectTypeCount: {
type: 'number',
description: 'Number of object types',
optional: true,
},
},
},
},
total: { type: 'number', description: 'Total number of schemas' },
isLast: { type: 'boolean', description: 'Whether this is the last page' },
},
}
+117
View File
@@ -0,0 +1,117 @@
import type { JsmListObjectTypesParams, JsmListObjectTypesResponse } from '@/tools/jsm/types'
import type { ToolConfig } from '@/tools/types'
export const jsmListObjectTypesTool: ToolConfig<
JsmListObjectTypesParams,
JsmListObjectTypesResponse
> = {
id: 'jsm_list_object_types',
name: 'JSM List Asset Object Types',
description: 'List object types within an Assets (Insight/CMDB) object schema',
version: '1.0.0',
oauth: {
required: true,
provider: 'jira',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'OAuth access token for Jira Service Management',
},
domain: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Your Jira domain (e.g., yourcompany.atlassian.net)',
},
cloudId: {
type: 'string',
required: false,
visibility: 'hidden',
description: 'Jira Cloud ID for the instance',
},
workspaceId: {
type: 'string',
required: false,
visibility: 'hidden',
description: 'Assets workspace ID (resolved automatically when omitted)',
},
schemaId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The Assets object schema ID to list object types for',
},
excludeAbstract: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Exclude abstract object types from the result',
},
},
request: {
url: '/api/tools/jsm/assets/object-types',
method: 'POST',
headers: () => ({ 'Content-Type': 'application/json' }),
body: (params) => ({
domain: params.domain,
accessToken: params.accessToken,
cloudId: params.cloudId,
workspaceId: params.workspaceId,
schemaId: params.schemaId?.trim(),
excludeAbstract: params.excludeAbstract,
}),
},
transformResponse: async (response: Response) => {
const responseText = await response.text()
if (!responseText) {
return {
success: false,
output: { ts: new Date().toISOString(), objectTypes: [], total: 0 },
error: 'Empty response from API',
}
}
const data = JSON.parse(responseText)
if (data.success && data.output) return data
return {
success: data.success || false,
output: data.output || { ts: new Date().toISOString(), objectTypes: [], total: 0 },
error: data.error,
}
},
outputs: {
ts: { type: 'string', description: 'Timestamp of the operation' },
objectTypes: {
type: 'array',
description: 'List of object types in the schema',
items: {
type: 'object',
properties: {
id: { type: 'string', description: 'Object type ID' },
name: { type: 'string', description: 'Object type name' },
description: { type: 'string', description: 'Object type description', optional: true },
objectSchemaId: { type: 'string', description: 'Parent schema ID' },
objectCount: { type: 'number', description: 'Number of objects', optional: true },
abstractObjectType: {
type: 'boolean',
description: 'Whether the type is abstract',
optional: true,
},
inherited: {
type: 'boolean',
description: 'Whether the type inherits attributes',
optional: true,
},
},
},
},
total: { type: 'number', description: 'Total number of object types' },
},
}
+150
View File
@@ -0,0 +1,150 @@
import type { JsmSearchObjectsAqlParams, JsmSearchObjectsAqlResponse } from '@/tools/jsm/types'
import type { ToolConfig } from '@/tools/types'
export const jsmSearchObjectsAqlTool: ToolConfig<
JsmSearchObjectsAqlParams,
JsmSearchObjectsAqlResponse
> = {
id: 'jsm_search_objects_aql',
name: 'JSM Search Assets (AQL)',
description:
'Search Assets (Insight/CMDB) objects using AQL (Assets Query Language), e.g. objectType = "Host" AND Status = "Running". Supports pagination.',
version: '1.0.0',
oauth: {
required: true,
provider: 'jira',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'OAuth access token for Jira Service Management',
},
domain: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Your Jira domain (e.g., yourcompany.atlassian.net)',
},
cloudId: {
type: 'string',
required: false,
visibility: 'hidden',
description: 'Jira Cloud ID for the instance',
},
workspaceId: {
type: 'string',
required: false,
visibility: 'hidden',
description: 'Assets workspace ID (resolved automatically when omitted)',
},
qlQuery: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'AQL query string (e.g., objectType = "Host" AND "Operating System" = "Ubuntu")',
},
page: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Page number (1-based, defaults to 1)',
},
resultsPerPage: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Results per page (e.g., 25, 50)',
},
includeAttributes: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Include resolved attribute values on each object (defaults to true)',
},
objectTypeId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Optionally scope the search to a single object type ID',
},
objectSchemaId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Optionally scope the search to a single object schema ID',
},
},
request: {
url: '/api/tools/jsm/assets/search',
method: 'POST',
headers: () => ({ 'Content-Type': 'application/json' }),
body: (params) => ({
domain: params.domain,
accessToken: params.accessToken,
cloudId: params.cloudId,
workspaceId: params.workspaceId,
qlQuery: params.qlQuery,
page: params.page,
resultsPerPage: params.resultsPerPage,
includeAttributes: params.includeAttributes,
objectTypeId: params.objectTypeId,
objectSchemaId: params.objectSchemaId,
}),
},
transformResponse: async (response: Response) => {
const responseText = await response.text()
if (!responseText) {
return {
success: false,
output: {
ts: new Date().toISOString(),
objects: [],
total: 0,
pageNumber: 0,
pageSize: 0,
},
error: 'Empty response from API',
}
}
const data = JSON.parse(responseText)
if (data.success && data.output) return data
return {
success: data.success || false,
output: data.output || {
ts: new Date().toISOString(),
objects: [],
total: 0,
pageNumber: 0,
pageSize: 0,
},
error: data.error,
}
},
outputs: {
ts: { type: 'string', description: 'Timestamp of the operation' },
objects: {
type: 'array',
description: 'Matching Assets objects',
items: {
type: 'object',
properties: {
id: { type: 'string', description: 'Object ID' },
label: { type: 'string', description: 'Object label', optional: true },
objectKey: { type: 'string', description: 'Object key (e.g., HOST-123)', optional: true },
objectType: { type: 'json', description: 'Object type metadata', optional: true },
attributes: { type: 'json', description: 'Resolved attribute values', optional: true },
},
},
},
total: { type: 'number', description: 'Total number of matching objects (totalFilterCount)' },
pageNumber: { type: 'number', description: 'Current page number' },
pageSize: { type: 'number', description: 'Number of objects on this page' },
},
}
+226
View File
@@ -1080,3 +1080,229 @@ export type JsmResponse =
| JsmCopyFormsResponse
| JsmGetFormAnswersResponse
| JsmReopenFormResponse
| JsmListObjectSchemasResponse
| JsmGetObjectSchemaResponse
| JsmListObjectTypesResponse
| JsmGetObjectTypeAttributesResponse
| JsmSearchObjectsAqlResponse
| JsmGetObjectResponse
| JsmCreateObjectResponse
| JsmUpdateObjectResponse
| JsmDeleteObjectResponse
/**
* JSM Assets (Insight / CMDB) tool types.
*
* The Assets API is keyed by an Assets `workspaceId` (resolved server-side from
* the Jira `cloudId`). All tools share {@link JsmAssetsBaseParams}.
*/
/** Base params shared by every JSM Assets tool */
export interface JsmAssetsBaseParams {
accessToken: string
domain: string
/** Jira Cloud ID (resolved server-side from the domain when omitted) */
cloudId?: string
/** Assets workspace ID (resolved server-side from the cloudId when omitted) */
workspaceId?: string
}
/** A single attribute value entry on an Assets object */
export interface AssetObjectAttributeValue {
value: string | null
displayValue: string | null
searchValue?: string | null
referencedType?: boolean
referencedObject?: Record<string, unknown> | null
}
/** A resolved attribute on an Assets object (read shape) */
export interface AssetObjectAttribute {
id: string
objectTypeAttributeId: string
objectAttributeValues: AssetObjectAttributeValue[]
}
/** An Assets object as returned by get/create/update */
export interface AssetObject {
id: string
label: string | null
objectKey: string | null
globalId: string | null
created: string | null
updated: string | null
hasAvatar: boolean
objectType: Record<string, unknown> | null
attributes: AssetObjectAttribute[]
link: string | null
}
/** Attribute payload for creating/updating an Assets object */
export interface AssetObjectAttributeInput {
objectTypeAttributeId: string
objectAttributeValues: Array<{ value: unknown }>
}
/** Raw attribute value as returned by the Assets API (before normalization) */
export interface RawAssetObjectAttributeValue {
value?: string | null
displayValue?: string | null
searchValue?: string | null
referencedType?: boolean
referencedObject?: Record<string, unknown> | null
}
/** Raw attribute as returned by the Assets API (before normalization) */
export interface RawAssetObjectAttribute {
id?: string
objectTypeAttributeId?: string
objectAttributeValues?: RawAssetObjectAttributeValue[]
}
/** Raw Assets object as returned by get/create/update/AQL (before normalization) */
export interface RawAssetObject {
id: string
label?: string | null
objectKey?: string | null
globalId?: string | null
created?: string | null
updated?: string | null
hasAvatar?: boolean
objectType?: Record<string, unknown> | null
attributes?: RawAssetObjectAttribute[]
_links?: { self?: string } | null
}
/** Output property descriptors reused across Assets object responses */
export const ASSET_OBJECT_PROPERTIES = {
id: { type: 'string', description: 'Object ID' },
label: { type: 'string', description: 'Human-readable object label', optional: true },
objectKey: { type: 'string', description: 'Object key (e.g., HOST-123)', optional: true },
globalId: { type: 'string', description: 'Global object ID', optional: true },
objectType: { type: 'json', description: 'Object type metadata', optional: true },
attributes: { type: 'json', description: 'Resolved attribute values for the object' },
hasAvatar: { type: 'boolean', description: 'Whether the object has an avatar', optional: true },
created: { type: 'string', description: 'Creation timestamp', optional: true },
updated: { type: 'string', description: 'Last update timestamp', optional: true },
link: { type: 'string', description: 'Self link to the object', optional: true },
} as const
export interface JsmListObjectSchemasParams extends JsmAssetsBaseParams {
startAt?: number
maxResults?: number
includeCounts?: boolean
}
export interface JsmListObjectSchemasResponse extends ToolResponse {
output: {
ts: string
schemas: Array<Record<string, unknown>>
total: number
isLast: boolean
}
}
export interface JsmGetObjectSchemaParams extends JsmAssetsBaseParams {
schemaId: string
}
export interface JsmGetObjectSchemaResponse extends ToolResponse {
output: {
ts: string
schema: Record<string, unknown> | null
}
}
export interface JsmListObjectTypesParams extends JsmAssetsBaseParams {
schemaId: string
excludeAbstract?: boolean
}
export interface JsmListObjectTypesResponse extends ToolResponse {
output: {
ts: string
objectTypes: Array<Record<string, unknown>>
total: number
}
}
export interface JsmGetObjectTypeAttributesParams extends JsmAssetsBaseParams {
objectTypeId: string
onlyValueEditable?: boolean
query?: string
}
export interface JsmGetObjectTypeAttributesResponse extends ToolResponse {
output: {
ts: string
attributes: Array<Record<string, unknown>>
total: number
}
}
export interface JsmSearchObjectsAqlParams extends JsmAssetsBaseParams {
qlQuery: string
page?: number
resultsPerPage?: number
includeAttributes?: boolean
objectTypeId?: string
objectSchemaId?: string
}
export interface JsmSearchObjectsAqlResponse extends ToolResponse {
output: {
ts: string
objects: Array<Record<string, unknown>>
total: number
pageNumber: number
pageSize: number
}
}
export interface JsmGetObjectParams extends JsmAssetsBaseParams {
objectId: string
}
export interface JsmGetObjectResponse extends ToolResponse {
output: {
ts: string
object: AssetObject | null
}
}
export interface JsmCreateObjectParams extends JsmAssetsBaseParams {
objectTypeId: string
attributes: AssetObjectAttributeInput[]
}
export interface JsmCreateObjectResponse extends ToolResponse {
output: {
ts: string
object: AssetObject | null
}
}
export interface JsmUpdateObjectParams extends JsmAssetsBaseParams {
objectId: string
attributes: AssetObjectAttributeInput[]
objectTypeId?: string
}
export interface JsmUpdateObjectResponse extends ToolResponse {
output: {
ts: string
object: AssetObject | null
}
}
export interface JsmDeleteObjectParams extends JsmAssetsBaseParams {
objectId: string
}
export interface JsmDeleteObjectResponse extends ToolResponse {
output: {
ts: string
objectId: string
deleted: boolean
}
}
+104
View File
@@ -0,0 +1,104 @@
import type { JsmUpdateObjectParams, JsmUpdateObjectResponse } from '@/tools/jsm/types'
import { ASSET_OBJECT_PROPERTIES } from '@/tools/jsm/types'
import type { ToolConfig } from '@/tools/types'
export const jsmUpdateObjectTool: ToolConfig<JsmUpdateObjectParams, JsmUpdateObjectResponse> = {
id: 'jsm_update_object',
name: 'JSM Update Asset Object',
description:
'Update an existing Assets (Insight/CMDB) object. Provide the attributes to change using their objectTypeAttributeId values.',
version: '1.0.0',
oauth: {
required: true,
provider: 'jira',
},
params: {
accessToken: {
type: 'string',
required: true,
visibility: 'hidden',
description: 'OAuth access token for Jira Service Management',
},
domain: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'Your Jira domain (e.g., yourcompany.atlassian.net)',
},
cloudId: {
type: 'string',
required: false,
visibility: 'hidden',
description: 'Jira Cloud ID for the instance',
},
workspaceId: {
type: 'string',
required: false,
visibility: 'hidden',
description: 'Assets workspace ID (resolved automatically when omitted)',
},
objectId: {
type: 'string',
required: true,
visibility: 'user-or-llm',
description: 'The Assets object ID to update',
},
attributes: {
type: 'json',
required: true,
visibility: 'user-or-llm',
description:
'Array of attributes to set: [{ objectTypeAttributeId, objectAttributeValues: [{ value }] }]',
},
objectTypeId: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Optional object type ID (only if changing the type)',
},
},
request: {
url: '/api/tools/jsm/assets/object/update',
method: 'POST',
headers: () => ({ 'Content-Type': 'application/json' }),
body: (params) => ({
domain: params.domain,
accessToken: params.accessToken,
cloudId: params.cloudId,
workspaceId: params.workspaceId,
objectId: params.objectId?.trim(),
objectTypeId: params.objectTypeId?.trim(),
attributes: params.attributes,
}),
},
transformResponse: async (response: Response) => {
const responseText = await response.text()
if (!responseText) {
return {
success: false,
output: { ts: new Date().toISOString(), object: null },
error: 'Empty response from API',
}
}
const data = JSON.parse(responseText)
if (data.success && data.output) return data
return {
success: data.success || false,
output: data.output || { ts: new Date().toISOString(), object: null },
error: data.error,
}
},
outputs: {
ts: { type: 'string', description: 'Timestamp of the operation' },
object: {
type: 'json',
description: 'The updated Assets object',
properties: ASSET_OBJECT_PROPERTIES,
},
},
}
+104
View File
@@ -2,6 +2,58 @@
* Shared utilities for Jira Service Management tools
*/
import { getJiraCloudId } from '@/tools/jira/utils'
import type { AssetObject, RawAssetObject } from '@/tools/jsm/types'
/**
* Resolve the Jira `cloudId` and Assets `workspaceId` needed for an Assets API
* call, using the request params when present and falling back to discovery.
* @param domain - The Jira site domain
* @param accessToken - The OAuth access token
* @param cloudIdParam - Optional cloudId already supplied by the caller
* @param workspaceIdParam - Optional workspaceId already supplied by the caller
*/
export async function resolveAssetsContext(
domain: string,
accessToken: string,
cloudIdParam?: string,
workspaceIdParam?: string
): Promise<{ cloudId: string; workspaceId: string }> {
const cloudId = cloudIdParam || (await getJiraCloudId(domain, accessToken))
const workspaceId = workspaceIdParam || (await getAssetsWorkspaceId(cloudId, accessToken))
return { cloudId, workspaceId }
}
/**
* Normalize a raw Assets object (from get/create/update) into the trimmed
* {@link AssetObject} shape returned by the tools.
* @param data - The raw object payload from the Assets API
*/
export function mapAssetObject(data: RawAssetObject): AssetObject {
return {
id: data.id,
label: data.label ?? null,
objectKey: data.objectKey ?? null,
globalId: data.globalId ?? null,
created: data.created ?? null,
updated: data.updated ?? null,
hasAvatar: data.hasAvatar ?? false,
objectType: data.objectType ?? null,
attributes: (data.attributes ?? []).map((attr) => ({
id: attr.id ?? '',
objectTypeAttributeId: attr.objectTypeAttributeId ?? '',
objectAttributeValues: (attr.objectAttributeValues ?? []).map((v) => ({
value: v.value ?? null,
displayValue: v.displayValue ?? null,
searchValue: v.searchValue ?? null,
referencedType: v.referencedType ?? false,
referencedObject: v.referencedObject ?? null,
})),
})),
link: data._links?.self ?? null,
}
}
/**
* Build the base URL for JSM Service Desk API
* @param cloudId - The Jira Cloud ID
@@ -33,3 +85,55 @@ export function getJsmHeaders(accessToken: string): Record<string, string> {
'X-ExperimentalApi': 'opt-in',
}
}
/**
* Build the base URL for the JSM Assets (Insight/CMDB) API.
*
* Uses the OAuth 2.0 (3LO) gateway form `/ex/jira/{cloudId}/...` — matching
* {@link getJsmApiBaseUrl} — keyed by both the Jira `cloudId` and the Assets
* `workspaceId` (resolved via {@link getAssetsWorkspaceId}).
* @param cloudId - The Jira Cloud ID
* @param workspaceId - The Assets workspace ID
* @returns The base URL for the Assets API (v1)
*/
export function getAssetsApiBaseUrl(cloudId: string, workspaceId: string): string {
return `https://api.atlassian.com/ex/jira/${cloudId}/jsm/assets/workspace/${workspaceId}/v1`
}
/**
* Resolve the Assets `workspaceId` for a Jira site.
*
* Calls the Service Desk discovery endpoint and uses the first workspace.
* Atlassian provisions a single Assets workspace per site, so this is the
* canonical workspace; callers on a multi-workspace site can pass an explicit
* `workspaceId` to {@link resolveAssetsContext} to override it. Requires the
* `read:servicedesk-request` scope (already granted by the `jira` provider).
* @param cloudId - The Jira Cloud ID
* @param accessToken - The OAuth access token
* @returns The Assets workspace ID for the site
* @throws If discovery fails or no workspace is provisioned
*/
export async function getAssetsWorkspaceId(cloudId: string, accessToken: string): Promise<string> {
const response = await fetch(
`https://api.atlassian.com/ex/jira/${cloudId}/rest/servicedeskapi/assets/workspace`,
{ method: 'GET', headers: getJsmHeaders(accessToken) }
)
if (!response.ok) {
const errorText = await response.text()
throw new Error(
`Failed to resolve Assets workspace: ${response.status} - ${errorText || response.statusText}`
)
}
const data = await response.json()
const workspaceId: string | undefined = data?.values?.[0]?.workspaceId
if (!workspaceId) {
throw new Error(
'No Assets workspace found for this site. Assets (Insight) may not be enabled on the Jira instance.'
)
}
return workspaceId
}
+18
View File
@@ -1691,9 +1691,11 @@ import {
jsmAnswerApprovalTool,
jsmAttachFormTool,
jsmCopyFormsTool,
jsmCreateObjectTool,
jsmCreateOrganizationTool,
jsmCreateRequestTool,
jsmDeleteFormTool,
jsmDeleteObjectTool,
jsmExternaliseFormTool,
jsmGetApprovalsTool,
jsmGetCommentsTool,
@@ -1703,6 +1705,9 @@ import {
jsmGetFormTemplatesTool,
jsmGetFormTool,
jsmGetIssueFormsTool,
jsmGetObjectSchemaTool,
jsmGetObjectTool,
jsmGetObjectTypeAttributesTool,
jsmGetOrganizationsTool,
jsmGetParticipantsTool,
jsmGetQueuesTool,
@@ -1714,10 +1719,14 @@ import {
jsmGetSlaTool,
jsmGetTransitionsTool,
jsmInternaliseFormTool,
jsmListObjectSchemasTool,
jsmListObjectTypesTool,
jsmReopenFormTool,
jsmSaveFormAnswersTool,
jsmSearchObjectsAqlTool,
jsmSubmitFormTool,
jsmTransitionRequestTool,
jsmUpdateObjectTool,
} from '@/tools/jsm'
import {
kalshiAmendOrderTool,
@@ -4228,6 +4237,15 @@ export const tools: Record<string, ToolConfig> = {
jsm_externalise_form: jsmExternaliseFormTool,
jsm_internalise_form: jsmInternaliseFormTool,
jsm_copy_forms: jsmCopyFormsTool,
jsm_list_object_schemas: jsmListObjectSchemasTool,
jsm_get_object_schema: jsmGetObjectSchemaTool,
jsm_list_object_types: jsmListObjectTypesTool,
jsm_get_object_type_attributes: jsmGetObjectTypeAttributesTool,
jsm_search_objects_aql: jsmSearchObjectsAqlTool,
jsm_get_object: jsmGetObjectTool,
jsm_create_object: jsmCreateObjectTool,
jsm_update_object: jsmUpdateObjectTool,
jsm_delete_object: jsmDeleteObjectTool,
kalshi_get_markets: kalshiGetMarketsTool,
kalshi_get_markets_v2: kalshiGetMarketsV2Tool,
kalshi_get_market: kalshiGetMarketTool,
+2 -2
View File
@@ -9,8 +9,8 @@ const QUERY_HOOKS_DIR = path.join(ROOT, 'apps/sim/hooks/queries')
const SELECTOR_HOOKS_DIR = path.join(ROOT, 'apps/sim/hooks/selectors')
const BASELINE = {
totalRoutes: 828,
zodRoutes: 828,
totalRoutes: 837,
zodRoutes: 837,
nonZodRoutes: 0,
} as const