generate_webapi_call
Generate HTTP requests, curl commands, and JavaScript examples for Dataverse WebAPI operations including CRUD operations, associations, actions, and functions with proper OData query parameters and headers.
Instructions
Generate HTTP requests, curl commands, and JavaScript examples for Dataverse WebAPI operations. Supports all CRUD operations, associations, actions, and functions with proper OData query parameters and headers.
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| actionOrFunctionName | No | Name of the action or function to call | |
| callerId | No | MSCRMCallerID header for impersonation | |
| count | No | Include count of records | |
| data | No | Data to send in request body for create/update operations | |
| entityId | No | Entity ID for single record operations | |
| entitySetName | No | Entity set name or logical entity name (e.g., 'account', 'contact') - will be automatically suffixed with 's' for Dataverse API URLs | |
| expand | No | Related entities to expand | |
| filter | No | OData filter expression | |
| ifMatch | No | If-Match header for conditional updates | |
| ifNoneMatch | No | If-None-Match header | |
| includeAuthHeader | No | Include Authorization header placeholder in output | |
| includeSolutionContext | No | Include current solution context in headers | |
| operation | Yes | Type of operation to perform | |
| orderby | No | OData orderby expression | |
| parameters | No | Parameters for action/function calls | |
| prefer | No | Prefer header values (e.g., ['return=representation', 'odata.include-annotations=*']) | |
| relatedEntityId | No | Related entity ID for associations | |
| relatedEntitySetName | No | Related entity set name for associations | |
| relationshipName | No | Relationship name for associate/disassociate operations | |
| select | No | Fields to select (e.g., ['name', 'emailaddress1']) | |
| skip | No | Number of records to skip | |
| top | No | Number of records to return |
Implementation Reference
- src/tools/webapi-tools.ts:553-862 (handler)The core handler function that processes input parameters, resolves entity metadata, builds OData queries and headers, formats the complete WebAPI request (HTTP method, URL, headers, body), generates curl and JS examples, and handles all CRUD, associate/disassociate, action/function operations.async (params: any) => { try { const config = (client as any).config as { dataverseUrl: string }; const baseUrl = config.dataverseUrl; let method = 'GET'; let endpoint = ''; let body: any = undefined; // Build headers const headerOptions: any = { prefer: params.prefer, ifMatch: params.ifMatch, ifNoneMatch: params.ifNoneMatch, callerId: params.callerId }; if (params.includeSolutionContext) { const solutionContext = client.getSolutionContext(); if (solutionContext) { headerOptions.solutionUniqueName = solutionContext.solutionUniqueName; } } const headers = generateHeaders(headerOptions); if (params.includeAuthHeader) { headers['Authorization'] = 'Bearer {ACCESS_TOKEN}'; } // Build endpoint based on operation type // Resolve actual entity metadata so URLs and payloads match the real schema let entityInfo: any = null; let formattedEntitySetName = ''; const targetEntitySetCache: Map<string, string> = new Map(); if (params.entitySetName) { try { entityInfo = await resolveEntityInfo(client, params.entitySetName); formattedEntitySetName = entityInfo?.entitySetName || formatEntitySetName(params.entitySetName); } catch { formattedEntitySetName = formatEntitySetName(params.entitySetName); } } switch (params.operation) { case 'retrieve': if (!params.entitySetName || !params.entityId) { throw new Error('entitySetName and entityId are required for retrieve operation'); } method = 'GET'; endpoint = `${formattedEntitySetName}(${params.entityId})`; let retrieveSelect = params.select; if ((!retrieveSelect || retrieveSelect.length === 0) && entityInfo) { retrieveSelect = [entityInfo.primaryIdAttribute].filter(Boolean) as string[]; if (entityInfo.primaryNameAttribute) retrieveSelect.push(entityInfo.primaryNameAttribute); } const retrieveQuery = buildODataQuery({ select: retrieveSelect, expand: params.expand }); endpoint += retrieveQuery; break; case 'retrieveMultiple': if (!params.entitySetName) { throw new Error('entitySetName is required for retrieveMultiple operation'); } method = 'GET'; endpoint = formattedEntitySetName; let listSelect = params.select; if ((!listSelect || listSelect.length === 0) && entityInfo) { listSelect = [entityInfo.primaryIdAttribute].filter(Boolean) as string[]; if (entityInfo.primaryNameAttribute) listSelect.push(entityInfo.primaryNameAttribute); } const retrieveMultipleQuery = buildODataQuery({ select: listSelect, filter: params.filter, orderby: params.orderby, top: params.top, skip: params.skip, expand: params.expand, count: params.count }); endpoint += retrieveMultipleQuery; break; case 'create': if (!params.entitySetName) { throw new Error('entitySetName is required for create operation'); } method = 'POST'; endpoint = formattedEntitySetName; // If data is provided, process @odata.bind; otherwise generate a schema-aligned sample body if (params.data) { body = processODataBindProperties(params.data, baseUrl, entityInfo); } else if (entityInfo) { body = await generateSampleBodyFromSchema( entityInfo, baseUrl, async (targetLogicalName: string) => await getTargetEntitySetName(client, targetEntitySetCache, targetLogicalName), 'create' ); // Ensure at least primary name is included if generation returned empty if (body && Object.keys(body).length === 0) { const primaryFromFlag = entityInfo.attributes?.find((a: any) => a?.IsPrimaryName)?.LogicalName; const primary = entityInfo.primaryNameAttribute || primaryFromFlag; if (primary) { body[primary] = `Sample ${entityInfo.logicalName}`; } } } else { body = {}; // fallback empty body } break; case 'update': if (!params.entitySetName || !params.entityId) { throw new Error('entitySetName and entityId are required for update operation'); } method = 'PATCH'; endpoint = `${formattedEntitySetName}(${params.entityId})`; // Process @odata.bind properties for associations/disassociations on update, or generate a schema-aligned sample body if (params.data) { body = processODataBindProperties(params.data, baseUrl, entityInfo); } else if (entityInfo) { body = await generateSampleBodyFromSchema( entityInfo, baseUrl, async (targetLogicalName: string) => await getTargetEntitySetName(client, targetEntitySetCache, targetLogicalName), 'update' ); // Ensure at least one field present if generation returned empty if (body && Object.keys(body).length === 0) { const primaryFromFlag = entityInfo.attributes?.find((a: any) => a?.IsPrimaryName)?.LogicalName; const primary = entityInfo.primaryNameAttribute || primaryFromFlag; if (primary) { body[primary] = `Updated ${entityInfo.logicalName}`; } } } else { body = {}; // fallback empty body } break; case 'delete': if (!params.entitySetName || !params.entityId) { throw new Error('entitySetName and entityId are required for delete operation'); } method = 'DELETE'; endpoint = `${formattedEntitySetName}(${params.entityId})`; break; case 'associate': if (!params.entitySetName || !params.entityId || !params.relationshipName || !params.relatedEntitySetName || !params.relatedEntityId) { throw new Error('entitySetName, entityId, relationshipName, relatedEntitySetName, and relatedEntityId are required for associate operation'); } const formattedRelatedEntitySetName = formatEntitySetName(params.relatedEntitySetName); method = 'POST'; endpoint = `${formattedEntitySetName}(${params.entityId})/${params.relationshipName}/$ref`; body = { "@odata.id": `${baseUrl}/api/data/v9.2/${formattedRelatedEntitySetName}(${params.relatedEntityId})` }; break; case 'disassociate': if (!params.entitySetName || !params.entityId || !params.relationshipName) { throw new Error('entitySetName, entityId, and relationshipName are required for disassociate operation'); } method = 'DELETE'; if (params.relatedEntityId) { endpoint = `${formattedEntitySetName}(${params.entityId})/${params.relationshipName}(${params.relatedEntityId})/$ref`; } else { endpoint = `${formattedEntitySetName}(${params.entityId})/${params.relationshipName}/$ref`; } break; case 'callAction': if (!params.actionOrFunctionName) { throw new Error('actionOrFunctionName is required for callAction operation'); } method = 'POST'; if (params.entitySetName && params.entityId) { endpoint = `${formattedEntitySetName}(${params.entityId})/Microsoft.Dynamics.CRM.${params.actionOrFunctionName}`; } else { endpoint = params.actionOrFunctionName; } body = params.parameters || {}; break; case 'callFunction': if (!params.actionOrFunctionName) { throw new Error('actionOrFunctionName is required for callFunction operation'); } method = 'GET'; let functionEndpoint = params.actionOrFunctionName; if (params.parameters && Object.keys(params.parameters).length > 0) { const paramStrings = Object.entries(params.parameters).map(([key, value]) => { if (typeof value === 'string') { return `${key}='${value}'`; } else { return `${key}=${value}`; } }); functionEndpoint += `(${paramStrings.join(',')})`; } if (params.entitySetName && params.entityId) { endpoint = `${formattedEntitySetName}(${params.entityId})/Microsoft.Dynamics.CRM.${functionEndpoint}`; } else { endpoint = functionEndpoint; } break; default: throw new Error(`Unsupported operation: ${params.operation}`); } // Final normalization: ensure all @odata.bind values are relative and keys use navigation properties if (body) { body = processODataBindProperties(body, baseUrl, entityInfo); } const webApiCall = formatWebAPICall(baseUrl, method, endpoint, headers, body); // Additional information let additionalInfo = '\n\n--- Additional Information ---\n'; additionalInfo += `Operation Type: ${params.operation}\n`; if (params.entitySetName) { additionalInfo += `Entity Set: ${params.entitySetName}\n`; additionalInfo += `Formatted Entity Set: ${formattedEntitySetName}\n`; additionalInfo += `Dataverse WebAPI Format: /api/data/v9.2/[entitySetName]s (note: 's' suffix required)\n`; } if (params.entityId) { additionalInfo += `Entity ID: ${params.entityId}\n`; } // Add @odata.bind information if present if (body && hasODataBindProperties(body)) { additionalInfo += '\n--- @odata.bind Usage Detected ---\n'; additionalInfo += 'This request uses @odata.bind syntax for relationship management:\n'; const examples = extractNavigationPropertyExamples(body); examples.forEach(example => { additionalInfo += `${example}\n`; }); additionalInfo += '\n@odata.bind Syntax Guide:\n'; additionalInfo += '• Associate on Create/Update: "navigationProperty@odata.bind": "/entitysets(id)"\n'; additionalInfo += '• Disassociate: "navigationProperty@odata.bind": null\n'; additionalInfo += '• Single-valued navigation properties: For many-to-one relationships\n'; additionalInfo += '• Collection-valued navigation properties: Use /$ref endpoints instead\n'; additionalInfo += '• Full URL format: "https://org.crm.dynamics.com/api/data/v9.2/accounts(id)"\n'; additionalInfo += '• Relative format: "/accounts(id)" (preferred; base URL is not used in @odata.bind)\n'; } // Include curl command let curlCommand = `curl -X ${method} \\\n`; curlCommand += ` "${baseUrl}/api/data/v9.2/${endpoint}" \\\n`; Object.entries(headers).forEach(([key, value]) => { curlCommand += ` -H "${key}: ${value}" \\\n`; }); if (body) { curlCommand += ` -d '${JSON.stringify(body)}'`; } else { curlCommand = curlCommand.slice(0, -3); // Remove trailing " \\" } additionalInfo += `\nCurl Command:\n${curlCommand}\n`; // Include JavaScript fetch example let fetchExample = `fetch('${baseUrl}/api/data/v9.2/${endpoint}', {\n`; fetchExample += ` method: '${method}',\n`; fetchExample += ` headers: ${JSON.stringify(headers, null, 4)},\n`; if (body) { fetchExample += ` body: JSON.stringify(${JSON.stringify(body, null, 4)})\n`; } fetchExample += `})\n.then(response => response.json())\n.then(data => console.log(data));`; additionalInfo += `\nJavaScript Fetch Example:\n${fetchExample}\n`; return { content: [ { type: "text", text: `${webApiCall}${additionalInfo}` } ] }; } catch (error) { return { content: [ { type: "text", text: `Error generating WebAPI call: ${error instanceof Error ? error.message : 'Unknown error'}` } ], isError: true }; } }
- src/tools/webapi-tools.ts:512-551 (schema)Zod-based input schema defining all parameters for the tool, including operation type, entity details, OData query options, request body, headers, and operation-specific fields.description: "Generate HTTP requests, curl commands, and JavaScript examples for Dataverse WebAPI operations. Supports all CRUD operations, associations, actions, and functions with proper OData query parameters and headers.", inputSchema: { operation: z.enum([ "retrieve", "retrieveMultiple", "create", "update", "delete", "associate", "disassociate", "callAction", "callFunction" ]).describe("Type of operation to perform"), entitySetName: z.string().optional().describe("Entity set name or logical entity name (e.g., 'account', 'contact') - will be automatically suffixed with 's' for Dataverse API URLs"), entityId: z.string().optional().describe("Entity ID for single record operations"), // OData query options select: z.array(z.string()).optional().describe("Fields to select (e.g., ['name', 'emailaddress1'])"), filter: z.string().optional().describe("OData filter expression"), orderby: z.string().optional().describe("OData orderby expression"), top: z.number().optional().describe("Number of records to return"), skip: z.number().optional().describe("Number of records to skip"), expand: z.string().optional().describe("Related entities to expand"), count: z.boolean().optional().describe("Include count of records"), // Request body for create/update operations data: z.record(z.any()).optional().describe("Data to send in request body for create/update operations"), // Header options prefer: z.array(z.string()).optional().describe("Prefer header values (e.g., ['return=representation', 'odata.include-annotations=*'])"), ifMatch: z.string().optional().describe("If-Match header for conditional updates"), ifNoneMatch: z.string().optional().describe("If-None-Match header"), callerId: z.string().optional().describe("MSCRMCallerID header for impersonation"), // Association/Disassociation options relationshipName: z.string().optional().describe("Relationship name for associate/disassociate operations"), relatedEntitySetName: z.string().optional().describe("Related entity set name for associations"), relatedEntityId: z.string().optional().describe("Related entity ID for associations"), // Action/Function options actionOrFunctionName: z.string().optional().describe("Name of the action or function to call"), parameters: z.record(z.any()).optional().describe("Parameters for action/function calls"), // Additional options includeSolutionContext: z.boolean().default(true).describe("Include current solution context in headers"), includeAuthHeader: z.boolean().default(false).describe("Include Authorization header placeholder in output") }
- src/tools/webapi-tools.ts:507-864 (registration)Factory function that registers the 'generate_webapi_call' tool on the MCP server with its schema and handler.export function generateWebAPICallTool(server: McpServer, client: DataverseClient) { server.registerTool( "generate_webapi_call", { title: "Generate Dataverse WebAPI Call", description: "Generate HTTP requests, curl commands, and JavaScript examples for Dataverse WebAPI operations. Supports all CRUD operations, associations, actions, and functions with proper OData query parameters and headers.", inputSchema: { operation: z.enum([ "retrieve", "retrieveMultiple", "create", "update", "delete", "associate", "disassociate", "callAction", "callFunction" ]).describe("Type of operation to perform"), entitySetName: z.string().optional().describe("Entity set name or logical entity name (e.g., 'account', 'contact') - will be automatically suffixed with 's' for Dataverse API URLs"), entityId: z.string().optional().describe("Entity ID for single record operations"), // OData query options select: z.array(z.string()).optional().describe("Fields to select (e.g., ['name', 'emailaddress1'])"), filter: z.string().optional().describe("OData filter expression"), orderby: z.string().optional().describe("OData orderby expression"), top: z.number().optional().describe("Number of records to return"), skip: z.number().optional().describe("Number of records to skip"), expand: z.string().optional().describe("Related entities to expand"), count: z.boolean().optional().describe("Include count of records"), // Request body for create/update operations data: z.record(z.any()).optional().describe("Data to send in request body for create/update operations"), // Header options prefer: z.array(z.string()).optional().describe("Prefer header values (e.g., ['return=representation', 'odata.include-annotations=*'])"), ifMatch: z.string().optional().describe("If-Match header for conditional updates"), ifNoneMatch: z.string().optional().describe("If-None-Match header"), callerId: z.string().optional().describe("MSCRMCallerID header for impersonation"), // Association/Disassociation options relationshipName: z.string().optional().describe("Relationship name for associate/disassociate operations"), relatedEntitySetName: z.string().optional().describe("Related entity set name for associations"), relatedEntityId: z.string().optional().describe("Related entity ID for associations"), // Action/Function options actionOrFunctionName: z.string().optional().describe("Name of the action or function to call"), parameters: z.record(z.any()).optional().describe("Parameters for action/function calls"), // Additional options includeSolutionContext: z.boolean().default(true).describe("Include current solution context in headers"), includeAuthHeader: z.boolean().default(false).describe("Include Authorization header placeholder in output") } }, async (params: any) => { try { const config = (client as any).config as { dataverseUrl: string }; const baseUrl = config.dataverseUrl; let method = 'GET'; let endpoint = ''; let body: any = undefined; // Build headers const headerOptions: any = { prefer: params.prefer, ifMatch: params.ifMatch, ifNoneMatch: params.ifNoneMatch, callerId: params.callerId }; if (params.includeSolutionContext) { const solutionContext = client.getSolutionContext(); if (solutionContext) { headerOptions.solutionUniqueName = solutionContext.solutionUniqueName; } } const headers = generateHeaders(headerOptions); if (params.includeAuthHeader) { headers['Authorization'] = 'Bearer {ACCESS_TOKEN}'; } // Build endpoint based on operation type // Resolve actual entity metadata so URLs and payloads match the real schema let entityInfo: any = null; let formattedEntitySetName = ''; const targetEntitySetCache: Map<string, string> = new Map(); if (params.entitySetName) { try { entityInfo = await resolveEntityInfo(client, params.entitySetName); formattedEntitySetName = entityInfo?.entitySetName || formatEntitySetName(params.entitySetName); } catch { formattedEntitySetName = formatEntitySetName(params.entitySetName); } } switch (params.operation) { case 'retrieve': if (!params.entitySetName || !params.entityId) { throw new Error('entitySetName and entityId are required for retrieve operation'); } method = 'GET'; endpoint = `${formattedEntitySetName}(${params.entityId})`; let retrieveSelect = params.select; if ((!retrieveSelect || retrieveSelect.length === 0) && entityInfo) { retrieveSelect = [entityInfo.primaryIdAttribute].filter(Boolean) as string[]; if (entityInfo.primaryNameAttribute) retrieveSelect.push(entityInfo.primaryNameAttribute); } const retrieveQuery = buildODataQuery({ select: retrieveSelect, expand: params.expand }); endpoint += retrieveQuery; break; case 'retrieveMultiple': if (!params.entitySetName) { throw new Error('entitySetName is required for retrieveMultiple operation'); } method = 'GET'; endpoint = formattedEntitySetName; let listSelect = params.select; if ((!listSelect || listSelect.length === 0) && entityInfo) { listSelect = [entityInfo.primaryIdAttribute].filter(Boolean) as string[]; if (entityInfo.primaryNameAttribute) listSelect.push(entityInfo.primaryNameAttribute); } const retrieveMultipleQuery = buildODataQuery({ select: listSelect, filter: params.filter, orderby: params.orderby, top: params.top, skip: params.skip, expand: params.expand, count: params.count }); endpoint += retrieveMultipleQuery; break; case 'create': if (!params.entitySetName) { throw new Error('entitySetName is required for create operation'); } method = 'POST'; endpoint = formattedEntitySetName; // If data is provided, process @odata.bind; otherwise generate a schema-aligned sample body if (params.data) { body = processODataBindProperties(params.data, baseUrl, entityInfo); } else if (entityInfo) { body = await generateSampleBodyFromSchema( entityInfo, baseUrl, async (targetLogicalName: string) => await getTargetEntitySetName(client, targetEntitySetCache, targetLogicalName), 'create' ); // Ensure at least primary name is included if generation returned empty if (body && Object.keys(body).length === 0) { const primaryFromFlag = entityInfo.attributes?.find((a: any) => a?.IsPrimaryName)?.LogicalName; const primary = entityInfo.primaryNameAttribute || primaryFromFlag; if (primary) { body[primary] = `Sample ${entityInfo.logicalName}`; } } } else { body = {}; // fallback empty body } break; case 'update': if (!params.entitySetName || !params.entityId) { throw new Error('entitySetName and entityId are required for update operation'); } method = 'PATCH'; endpoint = `${formattedEntitySetName}(${params.entityId})`; // Process @odata.bind properties for associations/disassociations on update, or generate a schema-aligned sample body if (params.data) { body = processODataBindProperties(params.data, baseUrl, entityInfo); } else if (entityInfo) { body = await generateSampleBodyFromSchema( entityInfo, baseUrl, async (targetLogicalName: string) => await getTargetEntitySetName(client, targetEntitySetCache, targetLogicalName), 'update' ); // Ensure at least one field present if generation returned empty if (body && Object.keys(body).length === 0) { const primaryFromFlag = entityInfo.attributes?.find((a: any) => a?.IsPrimaryName)?.LogicalName; const primary = entityInfo.primaryNameAttribute || primaryFromFlag; if (primary) { body[primary] = `Updated ${entityInfo.logicalName}`; } } } else { body = {}; // fallback empty body } break; case 'delete': if (!params.entitySetName || !params.entityId) { throw new Error('entitySetName and entityId are required for delete operation'); } method = 'DELETE'; endpoint = `${formattedEntitySetName}(${params.entityId})`; break; case 'associate': if (!params.entitySetName || !params.entityId || !params.relationshipName || !params.relatedEntitySetName || !params.relatedEntityId) { throw new Error('entitySetName, entityId, relationshipName, relatedEntitySetName, and relatedEntityId are required for associate operation'); } const formattedRelatedEntitySetName = formatEntitySetName(params.relatedEntitySetName); method = 'POST'; endpoint = `${formattedEntitySetName}(${params.entityId})/${params.relationshipName}/$ref`; body = { "@odata.id": `${baseUrl}/api/data/v9.2/${formattedRelatedEntitySetName}(${params.relatedEntityId})` }; break; case 'disassociate': if (!params.entitySetName || !params.entityId || !params.relationshipName) { throw new Error('entitySetName, entityId, and relationshipName are required for disassociate operation'); } method = 'DELETE'; if (params.relatedEntityId) { endpoint = `${formattedEntitySetName}(${params.entityId})/${params.relationshipName}(${params.relatedEntityId})/$ref`; } else { endpoint = `${formattedEntitySetName}(${params.entityId})/${params.relationshipName}/$ref`; } break; case 'callAction': if (!params.actionOrFunctionName) { throw new Error('actionOrFunctionName is required for callAction operation'); } method = 'POST'; if (params.entitySetName && params.entityId) { endpoint = `${formattedEntitySetName}(${params.entityId})/Microsoft.Dynamics.CRM.${params.actionOrFunctionName}`; } else { endpoint = params.actionOrFunctionName; } body = params.parameters || {}; break; case 'callFunction': if (!params.actionOrFunctionName) { throw new Error('actionOrFunctionName is required for callFunction operation'); } method = 'GET'; let functionEndpoint = params.actionOrFunctionName; if (params.parameters && Object.keys(params.parameters).length > 0) { const paramStrings = Object.entries(params.parameters).map(([key, value]) => { if (typeof value === 'string') { return `${key}='${value}'`; } else { return `${key}=${value}`; } }); functionEndpoint += `(${paramStrings.join(',')})`; } if (params.entitySetName && params.entityId) { endpoint = `${formattedEntitySetName}(${params.entityId})/Microsoft.Dynamics.CRM.${functionEndpoint}`; } else { endpoint = functionEndpoint; } break; default: throw new Error(`Unsupported operation: ${params.operation}`); } // Final normalization: ensure all @odata.bind values are relative and keys use navigation properties if (body) { body = processODataBindProperties(body, baseUrl, entityInfo); } const webApiCall = formatWebAPICall(baseUrl, method, endpoint, headers, body); // Additional information let additionalInfo = '\n\n--- Additional Information ---\n'; additionalInfo += `Operation Type: ${params.operation}\n`; if (params.entitySetName) { additionalInfo += `Entity Set: ${params.entitySetName}\n`; additionalInfo += `Formatted Entity Set: ${formattedEntitySetName}\n`; additionalInfo += `Dataverse WebAPI Format: /api/data/v9.2/[entitySetName]s (note: 's' suffix required)\n`; } if (params.entityId) { additionalInfo += `Entity ID: ${params.entityId}\n`; } // Add @odata.bind information if present if (body && hasODataBindProperties(body)) { additionalInfo += '\n--- @odata.bind Usage Detected ---\n'; additionalInfo += 'This request uses @odata.bind syntax for relationship management:\n'; const examples = extractNavigationPropertyExamples(body); examples.forEach(example => { additionalInfo += `${example}\n`; }); additionalInfo += '\n@odata.bind Syntax Guide:\n'; additionalInfo += '• Associate on Create/Update: "navigationProperty@odata.bind": "/entitysets(id)"\n'; additionalInfo += '• Disassociate: "navigationProperty@odata.bind": null\n'; additionalInfo += '• Single-valued navigation properties: For many-to-one relationships\n'; additionalInfo += '• Collection-valued navigation properties: Use /$ref endpoints instead\n'; additionalInfo += '• Full URL format: "https://org.crm.dynamics.com/api/data/v9.2/accounts(id)"\n'; additionalInfo += '• Relative format: "/accounts(id)" (preferred; base URL is not used in @odata.bind)\n'; } // Include curl command let curlCommand = `curl -X ${method} \\\n`; curlCommand += ` "${baseUrl}/api/data/v9.2/${endpoint}" \\\n`; Object.entries(headers).forEach(([key, value]) => { curlCommand += ` -H "${key}: ${value}" \\\n`; }); if (body) { curlCommand += ` -d '${JSON.stringify(body)}'`; } else { curlCommand = curlCommand.slice(0, -3); // Remove trailing " \\" } additionalInfo += `\nCurl Command:\n${curlCommand}\n`; // Include JavaScript fetch example let fetchExample = `fetch('${baseUrl}/api/data/v9.2/${endpoint}', {\n`; fetchExample += ` method: '${method}',\n`; fetchExample += ` headers: ${JSON.stringify(headers, null, 4)},\n`; if (body) { fetchExample += ` body: JSON.stringify(${JSON.stringify(body, null, 4)})\n`; } fetchExample += `})\n.then(response => response.json())\n.then(data => console.log(data));`; additionalInfo += `\nJavaScript Fetch Example:\n${fetchExample}\n`; return { content: [ { type: "text", text: `${webApiCall}${additionalInfo}` } ] }; } catch (error) { return { content: [ { type: "text", text: `Error generating WebAPI call: ${error instanceof Error ? error.message : 'Unknown error'}` } ], isError: true }; } } ); }
- src/index.ts:231-231 (registration)Invocation of the registration factory in the main index file.generateWebAPICallTool(server, dataverseClient);
- src/tools/webapi-tools.ts:107-229 (helper)Key helper for processing @odata.bind properties in request bodies, normalizing URLs and correcting navigation property names.function processODataBindProperties( data: any, baseUrl: string, entityInfo?: { lookupNavMap?: Map<string, string>; attributes?: any[]; } ): any { if (!data || typeof data !== 'object') return data; const processedData: Record<string, any> = { ...data }; // Build quick lookup sets/maps for corrections const hasSchema = !!entityInfo; const navMap = entityInfo?.lookupNavMap || new Map<string, string>(); const attrToNavLower = new Map<string, string>(); for (const [attr, nav] of navMap.entries()) { attrToNavLower.set(String(attr).toLowerCase(), String(nav)); } const isLookupAttr = (logicalName: string): boolean => { if (!entityInfo?.attributes) return false; const ln = logicalName.toLowerCase(); const a = entityInfo.attributes.find((x: any) => String(x?.LogicalName).toLowerCase() === ln); return !!a && String(a.AttributeType).toLowerCase() === 'lookup'; }; // Helper to normalize values to relative "/entityset(id)" path const normalizeBindValue = (val: string): string => { if (!val || typeof val !== 'string') return val as any; // Strip full base URL + /api/data/v9.2 if present if (val.startsWith('http')) { const m = val.match(/\/api\/data\/v9\.2\/([^?]+)$/i); if (m && m[1]) { return `/${m[1]}`; } // Fallback: keep last path segment if it looks like "entityset(guid)" const last = val.split('/').pop() || ''; if (/^[a-z0-9_]+\([^)]*\)$/i.test(last)) { return `/${last}`; } return val; // unknown absolute, return as-is } // Strip leading /api/data/v9.2 if (val.startsWith('/api/data/v9.2/')) { return `/${val.substring('/api/data/v9.2/'.length)}`; } // Ensure leading slash if (!val.startsWith('/')) { return `/${val}`; } return val; }; // First pass: correct keys that already use @odata.bind for (const key of Object.keys(processedData)) { if (!key.includes('@odata.bind')) continue; const value = processedData[key]; // Keep null for disassociation if (value === null) { continue; } const rawProp = key.replace('@odata.bind', ''); let correctedProp = rawProp; // If user used attribute logical name for a lookup instead of nav property, fix it if (hasSchema) { const nav = attrToNavLower.get(rawProp.toLowerCase()); if (nav && nav !== rawProp) { correctedProp = nav; } } // If corrected, move value under the corrected key const targetKey = `${correctedProp}@odata.bind`; if (targetKey !== key) { // Only move if targetKey not already present if (processedData[targetKey] === undefined) { processedData[targetKey] = processedData[key]; } delete processedData[key]; } } // Second pass: normalize values to relative paths and upgrade plain logical-name keys when possible for (const key of Object.keys(processedData)) { const val = processedData[key]; if (key.includes('@odata.bind')) { if (typeof val === 'string') { processedData[key] = normalizeBindValue(val); } continue; } // If user passed a lookup logical name without @odata.bind, and the value looks like an entity ref, // upgrade to "<nav>@odata.bind": "/entityset(guid)" if (hasSchema && isLookupAttr(key)) { const maybeStr = processedData[key]; if (typeof maybeStr === 'string') { const looksLikeRef = maybeStr.startsWith('http') || maybeStr.startsWith('/api/data/v9.2/') || maybeStr.startsWith('/') || /^[a-z0-9_]+\([^)]*\)$/i.test(maybeStr); if (looksLikeRef) { const nav = attrToNavLower.get(key.toLowerCase()); if (nav) { const newKey = `${nav}@odata.bind`; if (processedData[newKey] === undefined) { processedData[newKey] = normalizeBindValue(maybeStr); delete processedData[key]; } } } } } } return processedData; }