#!/usr/bin/env node
const { Server } = require('@modelcontextprotocol/sdk/server/index.js');
const { StdioServerTransport } = require('@modelcontextprotocol/sdk/server/stdio.js');
const {
CallToolRequestSchema,
ListToolsRequestSchema,
} = require('@modelcontextprotocol/sdk/types.js');
const axios = require('axios');
// Configuration and utilities
function getConfig() {
const workspaceId = process.env.DXT_CONFIG_workspaceId || process.env.MCP_CONFIG_workspaceId || process.env.JSM_WORKSPACE_ID;
const authToken = process.env.DXT_CONFIG_authToken || process.env.MCP_CONFIG_authToken || process.env.JSM_AUTH_TOKEN;
const baseUrl = process.env.DXT_CONFIG_baseUrl || process.env.MCP_CONFIG_baseUrl || process.env.JSM_BASE_URL || 'https://api.atlassian.com/jsm/assets/workspace';
const enableDebug = process.env.DXT_CONFIG_enableDebug === 'true' || process.env.MCP_CONFIG_enableDebug === 'true' || process.env.DEBUG === 'true';
if (!workspaceId) {
throw new Error('JSM Workspace ID is required (workspaceId configuration)');
}
if (!authToken) {
throw new Error('JSM Authentication Token is required (authToken configuration)');
}
return { workspaceId, authToken, baseUrl, enableDebug };
}
function logDebug(message, data) {
try {
const config = getConfig();
if (config.enableDebug) {
console.error(`[DEBUG] ${message}`, data ? JSON.stringify(data, null, 2) : '');
}
} catch (error) {
// If config fails, still allow debug logging with environment variable
if (process.env.DEBUG === 'true' || process.env.DXT_CONFIG_enableDebug === 'true') {
console.error(`[DEBUG] ${message}`, data ? JSON.stringify(data, null, 2) : '');
}
}
}
function formatError(error) {
if (error instanceof Error) {
return error.message;
}
if (typeof error === 'object' && error !== null) {
if ('message' in error) {
return error.message;
}
if ('details' in error && error.details) {
return JSON.stringify(error.details);
}
return JSON.stringify(error);
}
return String(error);
}
// JSM API Client
class JsmClient {
constructor(config) {
this.config = config;
this.client = axios.create({
baseURL: `${config.baseUrl}/${config.workspaceId}/v1`,
headers: {
'Authorization': config.authToken,
'Content-Type': 'application/json',
'Accept': 'application/json'
},
timeout: 30000
});
this.client.interceptors.response.use(
(response) => response,
(error) => {
const apiError = {
message: error.message,
status: error.response?.status || 500,
details: error.response?.data
};
throw apiError;
}
);
}
async searchAssetsAql(request) {
try {
const response = await this.client.post('/object/aql', {
qlQuery: request.qlQuery,
startAt: request.startAt || 0,
maxResults: request.maxResults || 1000
}, {
params: {
startAt: request.startAt || 0,
maxResults: request.maxResults || 1000
}
});
return response.data;
} catch (error) {
console.error('Error searching assets with AQL:', error);
throw error;
}
}
async getObjectSchemas() {
try {
const response = await this.client.get('/objectschema/list');
return response.data;
} catch (error) {
console.error('Error getting object schemas:', error);
throw error;
}
}
async getObjectTypes(schemaId) {
try {
const response = await this.client.get(`/objectschema/${schemaId}/objecttypes/flat`, {
params: {
includeObjectCounts: true
}
});
return response.data;
} catch (error) {
console.error('Error getting object types:', error);
throw error;
}
}
async getObjectAttributes(objectTypeId) {
try {
const response = await this.client.get(`/objecttype/${objectTypeId}/attributes`, {
params: {
excludeParentAttributes: false,
includeValueExist: true
}
});
return response.data;
} catch (error) {
console.error('Error getting object attributes:', error);
throw error;
}
}
async searchChildObjects(parentObjectType, filters) {
try {
let aqlQuery = `objectType IN objectTypeAndChildren("${parentObjectType}")`;
if (filters?.keyPrefix) {
aqlQuery += ` AND Key startswith ${filters.keyPrefix}`;
}
if (filters?.dateFrom && filters?.dateTo) {
aqlQuery += ` AND "Created" >= "${filters.dateFrom}" AND "Created" <= "${filters.dateTo}"`;
}
return await this.searchAssetsAql({
qlQuery: aqlQuery,
startAt: 0,
maxResults: 1000
});
} catch (error) {
console.error('Error searching child objects:', error);
throw error;
}
}
}
// Tool handlers
async function handleSearchAssetsAql(client, args) {
try {
const { aqlQuery, startAt = 0, maxResults = 1000 } = args;
if (!aqlQuery || aqlQuery.trim().length === 0) {
throw new Error('AQL query cannot be empty');
}
logDebug('Executing AQL search', { aqlQuery, startAt, maxResults });
const result = await client.searchAssetsAql({
qlQuery: aqlQuery,
startAt,
maxResults
});
logDebug('AQL search completed', {
total: result.total,
returned: result.values?.length
});
const summary = `Found ${result.total} assets (showing ${result.values?.length || 0} results)`;
let formattedResults = '';
if (result.values && result.values.length > 0) {
formattedResults = result.values.map((asset, index) => {
const attributes = asset.attributes
.map(attr =>
attr.objectAttributeValues
.map(val => `${val.displayValue}`)
.join(', ')
)
.filter(val => val.length > 0)
.join(' | ');
return `${startAt + index + 1}. ${asset.label} (${asset.objectKey})
Type: ${asset.objectType.name}
Created: ${asset.created}
Attributes: ${attributes || 'None'}`;
}).join('\n\n');
}
return {
content: [
{
type: 'text',
text: `${summary}\n\n${formattedResults}`
}
]
};
} catch (error) {
logDebug('AQL search error', error);
return {
content: [
{
type: 'text',
text: `Error searching assets with AQL: ${formatError(error)}`
}
]
};
}
}
async function handleGetObjectSchemas(client) {
try {
logDebug('Fetching object schemas');
const result = await client.getObjectSchemas();
logDebug('Object schemas fetched', {
count: result.objectschemas?.length
});
const summary = `Found ${result.objectschemas?.length || 0} object schemas`;
let formattedResults = '';
if (result.objectschemas && result.objectschemas.length > 0) {
formattedResults = result.objectschemas.map((schema, index) => {
return `${index + 1}. ${schema.name} (ID: ${schema.id})
Key: ${schema.objectSchemaKey}
Status: ${schema.status}
Objects: ${schema.objectCount}
Object Types: ${schema.objectTypeCount}
Created: ${schema.created}
Updated: ${schema.updated}`;
}).join('\n\n');
}
return {
content: [
{
type: 'text',
text: `${summary}\n\n${formattedResults}`
}
]
};
} catch (error) {
logDebug('Get object schemas error', error);
return {
content: [
{
type: 'text',
text: `Error getting object schemas: ${formatError(error)}`
}
]
};
}
}
async function handleGetObjectTypes(client, args) {
try {
const { schemaId } = args;
if (!Number.isInteger(schemaId) || schemaId <= 0) {
throw new Error('Schema ID must be a positive integer');
}
logDebug('Fetching object types', { schemaId });
const result = await client.getObjectTypes(schemaId);
logDebug('Object types fetched', {
count: result?.length,
schemaId
});
const summary = `Found ${result?.length || 0} object types for schema ${schemaId}`;
let formattedResults = '';
if (result && result.length > 0) {
const sortedTypes = result.sort((a, b) => a.position - b.position);
formattedResults = sortedTypes.map((type, index) => {
const parentInfo = type.parentObjectTypeId ?
` (Parent: ${type.parentObjectTypeId})` : '';
const abstractInfo = type.abstractObjectType ? ' [Abstract]' : '';
const inheritedInfo = type.inherited ? ' [Inherited]' : '';
return `${index + 1}. ${type.name} (ID: ${type.id})${parentInfo}${abstractInfo}${inheritedInfo}
Objects: ${type.objectCount}
Position: ${type.position}
Created: ${type.created}
Updated: ${type.updated}`;
}).join('\n\n');
}
return {
content: [
{
type: 'text',
text: `${summary}\n\n${formattedResults}`
}
]
};
} catch (error) {
logDebug('Get object types error', error);
return {
content: [
{
type: 'text',
text: `Error getting object types: ${formatError(error)}`
}
]
};
}
}
async function handleGetObjectAttributes(client, args) {
try {
const { objectTypeId } = args;
if (!Number.isInteger(objectTypeId) || objectTypeId <= 0) {
throw new Error('Object Type ID must be a positive integer');
}
logDebug('Fetching object attributes', { objectTypeId });
const result = await client.getObjectAttributes(objectTypeId);
logDebug('Object attributes fetched', {
count: result?.length,
objectTypeId
});
const summary = `Found ${result?.length || 0} attributes for object type ${objectTypeId}`;
let formattedResults = '';
if (result && result.length > 0) {
const sortedAttributes = result.sort((a, b) => a.position - b.position);
formattedResults = sortedAttributes.map((attr, index) => {
const typeMap = {
0: 'Default', 1: 'Text', 2: 'Integer', 3: 'Boolean', 4: 'Double',
5: 'Date', 6: 'Time', 7: 'Date Time', 8: 'URL', 9: 'Email',
10: 'Textarea', 11: 'Select', 12: 'IP Address', 13: 'Reference',
14: 'User', 15: 'Group', 16: 'Version', 17: 'Project', 18: 'Status'
};
const typeInfo = typeMap[attr.type] || `Unknown (${attr.type})`;
const systemInfo = attr.system ? ' [System]' : '';
const requiredInfo = attr.minimumCardinality > 0 ? ' [Required]' : '';
const uniqueInfo = attr.uniqueAttribute ? ' [Unique]' : '';
const editableInfo = !attr.editable ? ' [Read-only]' : '';
const hiddenInfo = attr.hidden ? ' [Hidden]' : '';
const referenceInfo = attr.referenceObjectTypeId ?
`\n References: Object Type ${attr.referenceObjectTypeId}` : '';
return `${index + 1}. ${attr.label} (${attr.name})${systemInfo}${requiredInfo}${uniqueInfo}${editableInfo}${hiddenInfo}
Type: ${typeInfo}
Cardinality: ${attr.minimumCardinality}-${attr.maximumCardinality === -1 ? '∞' : attr.maximumCardinality}
Position: ${attr.position}${referenceInfo}
Sortable: ${attr.sortable ? 'Yes' : 'No'} | Indexed: ${attr.indexed ? 'Yes' : 'No'}`;
}).join('\n\n');
}
return {
content: [
{
type: 'text',
text: `${summary}\n\n${formattedResults}`
}
]
};
} catch (error) {
logDebug('Get object attributes error', error);
return {
content: [
{
type: 'text',
text: `Error getting object attributes: ${formatError(error)}`
}
]
};
}
}
async function handleSearchChildObjects(client, args) {
try {
const { parentObjectType, filters } = args;
if (!parentObjectType || parentObjectType.trim().length === 0) {
throw new Error('Parent object type cannot be empty');
}
logDebug('Searching child objects', { parentObjectType, filters });
const result = await client.searchChildObjects(parentObjectType, filters);
logDebug('Child objects search completed', {
total: result.total,
returned: result.values?.length,
parentObjectType
});
let filterInfo = '';
if (filters) {
const filterParts = [];
if (filters.keyPrefix) filterParts.push(`Key prefix: ${filters.keyPrefix}`);
if (filters.dateFrom && filters.dateTo) {
filterParts.push(`Date range: ${filters.dateFrom} to ${filters.dateTo}`);
}
if (filterParts.length > 0) {
filterInfo = ` (Filters: ${filterParts.join(', ')})`;
}
}
const summary = `Found ${result.total} child objects of "${parentObjectType}"${filterInfo} (showing ${result.values?.length || 0} results)`;
let formattedResults = '';
if (result.values && result.values.length > 0) {
formattedResults = result.values.map((asset, index) => {
const attributes = asset.attributes
.map(attr =>
attr.objectAttributeValues
.map(val => `${val.displayValue}`)
.join(', ')
)
.filter(val => val.length > 0)
.join(' | ');
const hierarchy = asset.objectType.parentObjectTypeId ?
` (Child of type ID: ${asset.objectType.parentObjectTypeId})` : '';
return `${index + 1}. ${asset.label} (${asset.objectKey})
Type: ${asset.objectType.name}${hierarchy}
Created: ${asset.created}
Updated: ${asset.updated}
Attributes: ${attributes || 'None'}`;
}).join('\n\n');
}
return {
content: [
{
type: 'text',
text: `${summary}\n\n${formattedResults}`
}
]
};
} catch (error) {
logDebug('Search child objects error', error);
return {
content: [
{
type: 'text',
text: `Error searching child objects: ${formatError(error)}`
}
]
};
}
}
// MCP Server
class JsmAssetsMcpServer {
constructor() {
this.server = new Server(
{
name: 'jsm-assets-mcp',
version: '1.0.0',
},
{
capabilities: {
tools: {},
},
}
);
try {
const config = getConfig();
this.jsmClient = new JsmClient(config);
logDebug('JSM Assets MCP Server initialized', {
workspaceId: config.workspaceId,
baseUrl: config.baseUrl
});
} catch (error) {
console.error('Failed to initialize JSM client:', error);
process.exit(1);
}
this.setupToolHandlers();
}
setupToolHandlers() {
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: 'search_assets_aql',
description: 'Search JSM Assets using AQL (Assets Query Language). Supports complex queries with filters and pagination.',
inputSchema: {
type: 'object',
properties: {
aqlQuery: {
type: 'string',
description: 'AQL query string (e.g., "objectType=\\"Installation Package\\" AND Key startswith IASM")'
},
startAt: {
type: 'number',
description: 'Starting index for pagination (default: 0)',
default: 0
},
maxResults: {
type: 'number',
description: 'Maximum number of results to return (default: 1000)',
default: 1000
}
},
required: ['aqlQuery']
}
},
{
name: 'get_object_schemas',
description: 'List all object schemas available in the JSM Assets workspace. Schemas contain related object types.',
inputSchema: {
type: 'object',
properties: {},
additionalProperties: false
}
},
{
name: 'get_object_types',
description: 'Get object types for a specific schema. Object types define the structure and properties of assets.',
inputSchema: {
type: 'object',
properties: {
schemaId: {
type: 'number',
description: 'The ID of the object schema to get types for'
}
},
required: ['schemaId']
}
},
{
name: 'get_object_attributes',
description: 'Get attributes (fields) for a specific object type. Attributes define what data can be stored for objects of this type.',
inputSchema: {
type: 'object',
properties: {
objectTypeId: {
type: 'number',
description: 'The ID of the object type to get attributes for'
}
},
required: ['objectTypeId']
}
},
{
name: 'search_child_objects',
description: 'Search for child objects of a specific parent object type, with optional filters for date range and key prefix.',
inputSchema: {
type: 'object',
properties: {
parentObjectType: {
type: 'string',
description: 'Name of the parent object type to search children for'
},
filters: {
type: 'object',
description: 'Optional filters to apply to the search',
properties: {
dateFrom: {
type: 'string',
description: 'Start date for filtering (format: YYYY-MM-DD HH:mm)',
pattern: '^\\d{4}-\\d{2}-\\d{2}( \\d{2}:\\d{2})?$'
},
dateTo: {
type: 'string',
description: 'End date for filtering (format: YYYY-MM-DD HH:mm)',
pattern: '^\\d{4}-\\d{2}-\\d{2}( \\d{2}:\\d{2})?$'
},
keyPrefix: {
type: 'string',
description: 'Key prefix to filter objects (e.g., "IASM" for keys starting with IASM)'
}
},
additionalProperties: false
}
},
required: ['parentObjectType']
}
}
],
};
});
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
switch (name) {
case 'search_assets_aql':
return await handleSearchAssetsAql(this.jsmClient, args);
case 'get_object_schemas':
return await handleGetObjectSchemas(this.jsmClient);
case 'get_object_types':
return await handleGetObjectTypes(this.jsmClient, args);
case 'get_object_attributes':
return await handleGetObjectAttributes(this.jsmClient, args);
case 'search_child_objects':
return await handleSearchChildObjects(this.jsmClient, args);
default:
throw new Error(`Unknown tool: ${name}`);
}
} catch (error) {
logDebug('Tool execution error', { name, error });
return {
content: [
{
type: 'text',
text: `Error executing tool ${name}: ${error instanceof Error ? error.message : String(error)}`,
},
],
};
}
});
}
async run() {
const transport = new StdioServerTransport();
await this.server.connect(transport);
console.error('JSM Assets MCP Server running on stdio');
logDebug('Server started and connected via stdio transport');
}
}
const server = new JsmAssetsMcpServer();
server.run().catch((error) => {
console.error('Fatal error running server:', error);
process.exit(1);
});