Skip to main content
Glama

Targetprocess MCP Server

inspect.tool.ts14.5 kB
import { McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js'; import { z } from 'zod'; import { TPService } from '../../api/client/tp.service.js'; import { EntityRegistry } from '../../core/entity-registry.js'; import { exec } from 'child_process'; import { promisify } from 'util'; import path from 'path'; import { logger } from '../../utils/logger.js'; const execAsync = promisify(exec); // Input schema for inspect object tool export const inspectObjectSchema = z.object({ action: z.string().describe('Action to perform: list_types, get_properties, get_property_details, or discover_api_structure'), entityType: z.string().optional().describe('Type of entity to inspect (required for get_properties and get_property_details)'), propertyName: z.string().optional().describe('Name of property to get details for (required for get_property_details)'), }); export type InspectObjectInput = z.infer<typeof inspectObjectSchema>; /** * Handler for the inspect object tool */ export class InspectObjectTool { constructor(private service: TPService) {} async execute(args: unknown) { try { const { action, entityType, propertyName } = inspectObjectSchema.parse(args); // Validate action const validActions = ['list_types', 'get_properties', 'get_property_details', 'discover_api_structure']; if (!validActions.includes(action)) { throw new McpError( ErrorCode.InvalidParams, `Invalid action: ${action}. Valid actions are: ${validActions.join(', ')}` ); } switch (action) { case 'list_types': return await this.listEntityTypes(); case 'get_properties': if (!entityType) { throw new McpError( ErrorCode.InvalidParams, 'entityType is required for get_properties action' ); } return await this.getEntityProperties(entityType); case 'get_property_details': if (!entityType || !propertyName) { throw new McpError( ErrorCode.InvalidParams, 'entityType and propertyName are required for get_property_details action' ); } return await this.getPropertyDetails(entityType, propertyName); case 'discover_api_structure': return await this.discoverApiStructure(); default: throw new McpError( ErrorCode.InvalidParams, `Unknown action: ${action}` ); } } catch (error) { if (error instanceof z.ZodError) { throw new McpError( ErrorCode.InvalidParams, `Invalid inspect object parameters: ${error.message}` ); } if (error instanceof McpError) { throw error; } throw new McpError( ErrorCode.InvalidRequest, `Inspect object failed: ${error instanceof Error ? error.message : String(error)}` ); } } /** * List all available entity types */ private async listEntityTypes() { try { // Fetch metadata from the API const response = await this.service.fetchMetadata(); // Extract entity types from the metadata const entityTypes = this.extractEntityTypes(response); return { content: [ { type: 'text', text: JSON.stringify(entityTypes, null, 2), }, ], }; } catch (error) { throw new McpError( ErrorCode.InvalidRequest, `Failed to list entity types: ${error instanceof Error ? error.message : String(error)}` ); } } /** * Get properties for a specific entity type */ private async getEntityProperties(entityType: string) { try { // Fetch metadata from the API const response = await this.service.fetchMetadata(); // Extract properties for the specified entity type const properties = this.extractEntityProperties(response, entityType); // Search documentation for additional context const docContext = await this.searchDocumentation(entityType); return { content: [ { type: 'text', text: JSON.stringify({ entityType, properties, documentation: docContext, }, null, 2), }, ], }; } catch (error) { throw new McpError( ErrorCode.InvalidRequest, `Failed to get properties for entity type ${entityType}: ${error instanceof Error ? error.message : String(error)}` ); } } /** * Get detailed information about a specific property */ private async getPropertyDetails(entityType: string, propertyName: string) { try { // Fetch metadata from the API const response = await this.service.fetchMetadata(); // Extract property details const propertyDetails = this.extractPropertyDetails(response, entityType, propertyName); // Search documentation for additional context const docContext = await this.searchDocumentation(`${entityType} ${propertyName}`); return { content: [ { type: 'text', text: JSON.stringify({ entityType, propertyName, details: propertyDetails, documentation: docContext, }, null, 2), }, ], }; } catch (error) { throw new McpError( ErrorCode.InvalidRequest, `Failed to get details for property ${propertyName} of entity type ${entityType}: ${error instanceof Error ? error.message : String(error)}` ); } } /** * Extract entity types from metadata */ private extractEntityTypes(metadata: any): string[] { // Implementation will depend on the structure of the metadata // This is a placeholder const entityTypes: string[] = []; if (metadata && metadata.Items) { for (const item of metadata.Items) { if (item.Name && !entityTypes.includes(item.Name)) { entityTypes.push(item.Name); } } } return entityTypes.sort(); } /** * Extract properties for a specific entity type from /EntityTypes endpoint response * Note: EntityTypes endpoint provides basic entity info but not detailed property metadata */ private extractEntityProperties(metadata: any, entityType: string): Record<string, any> { const properties: Record<string, any> = {}; if (metadata && metadata.Items) { const entityMeta = metadata.Items.find((item: any) => item.Name === entityType); if (entityMeta) { // EntityTypes endpoint provides basic entity information properties['basic_info'] = { name: entityMeta.Name, description: entityMeta.Description, isAssignable: entityMeta.IsAssignable, isGlobal: entityMeta.IsGlobal, supportsCustomFields: entityMeta.SupportsCustomFields, source: entityMeta.Source || 'API' }; // Add EntityRegistry information if available const entityInfo = EntityRegistry.getEntityTypeInfo(entityType); if (entityInfo) { properties['registry_info'] = { category: entityInfo.category, parentTypes: entityInfo.parentTypes, commonIncludes: entityInfo.commonIncludes, supportsCustomFields: entityInfo.supportsCustomFields }; } // Note: For detailed property metadata, use the original /meta endpoint // or implement a separate method that can handle the /meta endpoint's structure properties['note'] = 'EntityTypes endpoint provides basic entity information. For detailed property metadata, additional API calls to /meta endpoint may be needed.'; } } return properties; } /** * Extract detailed information about a specific property from /EntityTypes endpoint response * Note: EntityTypes endpoint doesn't provide detailed property metadata */ private extractPropertyDetails(metadata: any, entityType: string, propertyName: string): any { let propertyDetails: any = null; if (metadata && metadata.Items) { const entityMeta = metadata.Items.find((item: any) => item.Name === entityType); if (entityMeta) { // EntityTypes endpoint doesn't provide detailed property information // Return basic entity information and suggest using detailed metadata endpoint propertyDetails = { entityType: entityType, propertyName: propertyName, entityInfo: { name: entityMeta.Name, description: entityMeta.Description, isAssignable: entityMeta.IsAssignable, isGlobal: entityMeta.IsGlobal, supportsCustomFields: entityMeta.SupportsCustomFields, source: entityMeta.Source || 'API' }, note: 'EntityTypes endpoint does not provide detailed property metadata. For detailed property information, a separate API call to the /meta endpoint would be needed.', suggestion: 'Use entity introspection through actual API calls to discover available properties.' }; // Add EntityRegistry information if available const entityInfo = EntityRegistry.getEntityTypeInfo(entityType); if (entityInfo) { propertyDetails.registryInfo = { category: entityInfo.category, commonIncludes: entityInfo.commonIncludes, parentTypes: entityInfo.parentTypes }; // Check if propertyName is in common includes if (entityInfo.commonIncludes?.includes(propertyName)) { propertyDetails.likelyProperty = `${propertyName} is listed as a common include for ${entityType}, suggesting it's a related entity reference.`; } } } } return propertyDetails; } /** * Search documentation for additional context */ private async searchDocumentation(searchTerm: string): Promise<string> { try { const docsPath = path.resolve(__dirname, '../../../resources/target-process-docs'); const { stdout } = await execAsync(`cd ${docsPath} && ./search-docs.sh "${searchTerm}"`); // Extract relevant information from the search results return this.extractDocumentationContext(stdout); } catch (error) { logger.error('Error searching documentation:', error); return 'Documentation search failed or no results found.'; } } /** * Extract relevant information from documentation search results */ private extractDocumentationContext(searchResults: string): string { // Extract the most relevant parts of the search results // This is a simple implementation that returns the first 1000 characters return searchResults.substring(0, 1000) + (searchResults.length > 1000 ? '...' : ''); } /** * Discover API structure through controlled error triggering * This method intentionally triggers an error to extract entity type information */ private async discoverApiStructure() { try { // First try to get entity types directly if possible try { const metadata = await this.service.fetchMetadata(); const entityTypes = this.extractEntityTypes(metadata); if (entityTypes.length > 0) { return { content: [ { type: 'text', text: JSON.stringify({ entityTypes }, null, 2), }, ], }; } } catch (metadataError) { logger.error('Failed to fetch metadata directly:', metadataError); } // If direct method failed, try to trigger an informative error // by attempting to get a non-existent entity type try { await this.service.getEntity('NonExistentType', 1); // If no error, return empty result return { content: [ { type: 'text', text: JSON.stringify({ entityTypes: [] }, null, 2), }, ], }; } catch (error) { // Extract entity types from the error message const errorMessage = error instanceof Error ? error.message : String(error); const entityTypeMatch = errorMessage.match(/Valid entity types are: (.*)/); const entityTypes = entityTypeMatch && entityTypeMatch[1] ? entityTypeMatch[1].split(', ') : []; // Return just the extracted data return { content: [ { type: 'text', text: JSON.stringify({ entityTypes }, null, 2), }, ], }; } } catch (error) { // Handle any unexpected errors logger.error('Error in discoverApiStructure:', error); return { content: [ { type: 'text', text: JSON.stringify({ error: 'Failed to discover API structure', message: error instanceof Error ? error.message : String(error), entityTypes: [] }, null, 2), }, ], }; } } /** * Get tool definition for MCP */ static getDefinition() { return { name: 'inspect_object', description: 'Inspect TargetProcess API metadata. Use "list_types" to see all entity types, "get_properties" to see fields for an entity type, "discover_api_structure" for quick entity discovery.', inputSchema: { type: 'object', properties: { action: { type: 'string', description: 'Action: "list_types" (all entities), "get_properties" (fields for entity), "get_property_details" (field details), "discover_api_structure" (quick discovery)', }, entityType: { type: 'string', description: 'Type of entity to inspect (required for get_properties and get_property_details)', }, propertyName: { type: 'string', description: 'Name of property to get details for (required for get_property_details)', }, }, required: ['action'], }, } as const; } }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/aaronsb/apptio-target-process-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server